Merge a98f249fab34d1997d74005769308c0b82af4516 on remote branch

Change-Id: I6a77fcc93360e31530d8eea428f41e19b435b6e3
diff --git a/car-apps-common/src/com/android/car/apps/common/ControlBar.java b/car-apps-common/src/com/android/car/apps/common/ControlBar.java
index 4181867..94fb206 100644
--- a/car-apps-common/src/com/android/car/apps/common/ControlBar.java
+++ b/car-apps-common/src/com/android/car/apps/common/ControlBar.java
@@ -32,6 +32,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewParent;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
 import android.widget.FrameLayout;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
@@ -103,6 +104,17 @@
     // Weight for the spacers used between buttons
     private static final float SPACERS_WEIGHT = 1f;
 
+    private final OnGlobalFocusChangeListener mFocusChangeListener =
+            (oldFocus, newFocus) -> {
+                // Collapse the control bar when it is expanded and loses focus.
+                boolean hasFocus = hasFocus();
+                if (mHasFocus && !hasFocus && mIsExpanded) {
+                    onExpandCollapse();
+                }
+                mHasFocus = hasFocus;
+            };
+
+
     public ControlBar(Context context) {
         super(context);
         init(context, null, 0, 0);
@@ -167,15 +179,6 @@
         mDefaultExpandCollapseView.setContentDescription(context.getString(
                 R.string.control_bar_expand_collapse_button));
         mDefaultExpandCollapseView.setOnClickListener(v -> onExpandCollapse());
-
-        // Collapse the control bar when it is expanded and loses focus.
-        getViewTreeObserver().addOnGlobalFocusChangeListener((oldFocus, newFocus) -> {
-            boolean hasFocus = hasFocus();
-            if (mHasFocus && !hasFocus && mIsExpanded) {
-                onExpandCollapse();
-            }
-            mHasFocus = hasFocus;
-        });
     }
 
     private int getSlotIndex(@SlotPosition int slotPosition) {
@@ -183,6 +186,18 @@
     }
 
     @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getViewTreeObserver().addOnGlobalFocusChangeListener(mFocusChangeListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener);
+        super.onDetachedFromWindow();
+    }
+
+    @Override
     public void setView(@Nullable View view, @SlotPosition int slotPosition) {
         if (view != null) {
             mFixedViews.put(slotPosition, view);
diff --git a/car-apps-common/src/com/android/car/apps/common/util/ViewUtils.java b/car-apps-common/src/com/android/car/apps/common/util/ViewUtils.java
index a6cc870..0e41a6d 100644
--- a/car-apps-common/src/com/android/car/apps/common/util/ViewUtils.java
+++ b/car-apps-common/src/com/android/car/apps/common/util/ViewUtils.java
@@ -39,13 +39,34 @@
  * Utility methods to operate over views.
  */
 public class ViewUtils {
+
+    /** Listener to take action when animations are done. */
+    public interface ViewAnimEndListener {
+        /**
+         * Called when the animation created by {@link #hideViewAnimated} or
+         * {@link #showHideViewAnimated} has reached its end.
+         */
+        void onAnimationEnd(View view);
+    }
+
+    /** Shows the view if show is set to true otherwise hides it. */
+    public static void showHideViewAnimated(boolean show, @NonNull View view, int duration,
+            @Nullable ViewAnimEndListener listener) {
+        if (show) {
+            showViewAnimated(view, duration, listener);
+        } else {
+            hideViewAnimated(view, duration, listener);
+        }
+    }
+
     /**
      * Hides a view using a fade-out animation
      *
      * @param view     {@link View} to be hidden
      * @param duration animation duration in milliseconds.
      */
-    public static void hideViewAnimated(@NonNull View view, int duration) {
+    public static void hideViewAnimated(@NonNull View view, int duration,
+            @Nullable ViewAnimEndListener listener) {
         // Cancel existing animation to avoid race condition
         // if show and hide are called at the same time
         view.animate().cancel();
@@ -56,12 +77,26 @@
             return;
         }
 
+        Animator.AnimatorListener hider = hideViewAfterAnimation(view);
         view.animate()
                 .setDuration(duration)
-                .setListener(hideViewAfterAnimation(view))
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        hider.onAnimationEnd(animation);
+                        if (listener != null) {
+                            listener.onAnimationEnd(view);
+                        }
+                    }
+                })
                 .alpha(0f);
     }
 
+    /** Hides a view using a fade-out animation. */
+    public static void hideViewAnimated(@NonNull View view, int duration) {
+        hideViewAnimated(view, duration, null);
+    }
+
     /** Returns an AnimatorListener that hides the view at the end. */
     public static Animator.AnimatorListener hideViewAfterAnimation(View view) {
         return new AnimatorListenerAdapter() {
@@ -79,20 +114,26 @@
      * @param duration animation duration in milliseconds.
      */
     public static void hideViewsAnimated(@Nullable List<View> views, int duration) {
+        if (views == null) {
+            return;
+        }
         for (View view : views) {
             if (view != null) {
-                hideViewAnimated(view, duration);
+                hideViewAnimated(view, duration, null);
             }
         }
     }
 
     /**
-     * Shows a view using a fade-in animation
+     * Shows a view using a fade-in animation. The view's alpha value isn't changed so that
+     * animating an already visible won't have a visible effect. Therefore, <b>views initialized as
+     * hidden must have their alpha set to 0 prior to calling this method</b>.
      *
      * @param view     {@link View} to be shown
      * @param duration animation duration in milliseconds.
      */
-    public static void showViewAnimated(@NonNull View view, int duration) {
+    public static void showViewAnimated(@NonNull View view, int duration,
+            @Nullable ViewAnimEndListener listener) {
         // Cancel existing animation to avoid race condition
         // if show and hide are called at the same time
         view.animate().cancel();
@@ -107,10 +148,21 @@
                     public void onAnimationStart(Animator animation) {
                         view.setVisibility(VISIBLE);
                     }
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        if (listener != null) {
+                            listener.onAnimationEnd(view);
+                        }
+                    }
                 })
                 .alpha(1f);
     }
 
+    /** Shows a view using a fade-in animation. */
+    public static void showViewAnimated(@NonNull View view, int duration) {
+        showViewAnimated(view, duration, null);
+    }
+
     /**
      * Shows views using a fade-out animation
      *
@@ -120,7 +172,7 @@
     public static void showViewsAnimated(@Nullable List<View> views, int duration) {
         for (View view : views) {
             if (view != null) {
-                showViewAnimated(view, duration);
+                showViewAnimated(view, duration, null);
             }
         }
     }
@@ -207,6 +259,16 @@
         return views;
     }
 
+    /** Removes the view from its parent (if it has one). */
+    public static void removeFromParent(@Nullable View view) {
+        if (view != null) {
+            ViewGroup parent = (ViewGroup) view.getParent();
+            if (parent != null) {
+                parent.removeView(view);
+            }
+        }
+    }
+
     /** Adds the {@code view} into the {@code container}. */
     public static void setView(@Nullable View view, FrameLayout container) {
         if (view != null) {
@@ -215,13 +277,11 @@
                 return;
             }
 
-            ViewGroup parent = (ViewGroup) view.getParent();
             // As we are removing views (on BT disconnect, for example), some items will be
             // shifting from expanded to collapsed (like Queue item) - remove those from the
             // group before adding to the new slot
-            if (view.getParent() != null) {
-                parent.removeView(view);
-            }
+            removeFromParent(view);
+
             container.removeAllViews();
             container.addView(view);
             container.setVisibility(VISIBLE);
diff --git a/car-arch-common/src/com/android/car/arch/common/FutureData.java b/car-arch-common/src/com/android/car/arch/common/FutureData.java
index d2ef248..9b5c41b 100644
--- a/car-arch-common/src/com/android/car/arch/common/FutureData.java
+++ b/car-arch-common/src/com/android/car/arch/common/FutureData.java
@@ -17,18 +17,44 @@
 package com.android.car.arch.common;
 
 /**
- * Class that holds data with a loading state.
+ * Class that holds data with a loading state, and optionally the previous version of the data.
  *
  * @param <T> the output data type
  */
 public class FutureData<T> {
 
     private final boolean mIsLoading;
+    private final T mPastData;
     private final T mData;
 
+    /** Returns an instance with a null data value and loading set to true. */
+    public static <T> FutureData<T> newLoadingData() {
+        return new FutureData<>(true, null);
+    }
+
+    /** Returns a loaded instance with the given data value. */
+    public static <T> FutureData<T> newLoadedData(T data) {
+        return new FutureData<>(false, data);
+    }
+
+    /** Returns a loaded instance with the given previous and current data values. */
+    public static <T> FutureData<T> newLoadedData(T oldData, T newData) {
+        return new FutureData<>(false, oldData, newData);
+    }
+
+    /**
+     * This should become private.
+     * @deprecated Use {@link #newLoadingData}, and {@link #newLoadedData} instead.
+     */
+    @Deprecated
     public FutureData(boolean isLoading, T data) {
+        this(isLoading, null, data);
+    }
+
+    private FutureData(boolean isLoading, T oldData, T newData) {
         mIsLoading = isLoading;
-        mData = data;
+        mPastData = oldData;
+        mData = newData;
     }
 
     /**
@@ -44,4 +70,9 @@
     public T getData() {
         return mData;
     }
+
+    /** If done loading, returns the previous version of the data, otherwise null. */
+    public T getPastData() {
+        return mPastData;
+    }
 }
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
index 1f9d915..f10d701 100644
--- a/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
+++ b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
@@ -69,6 +69,7 @@
 
     private final Context mContext;
     private final AssistUtils mAssistUtils;
+    @Nullable
     private final FallbackAssistant mFallbackAssistant;
     private final String mErrorMessage;
     private final boolean mIsFallbackAssistantEnabled;
@@ -106,10 +107,11 @@
     public CarAssistUtils(Context context) {
         mContext = context;
         mAssistUtils = new AssistUtils(context);
-        mFallbackAssistant = new FallbackAssistant(context);
         mErrorMessage = context.getString(R.string.assist_action_failed_toast);
+
         mIsFallbackAssistantEnabled =
                 context.getResources().getBoolean(R.bool.config_enableFallbackAssistant);
+        mFallbackAssistant = mIsFallbackAssistantEnabled ? new FallbackAssistant(context) : null;
     }
 
     /**
@@ -120,6 +122,13 @@
     }
 
     /**
+     * Returns {@code true} if the fallback assistant is enabled.
+     */
+    public boolean isFallbackAssistantEnabled() {
+        return mIsFallbackAssistantEnabled;
+    }
+
+    /**
      * Returns true if the current active assistant has notification listener permissions.
      */
     public boolean assistantIsNotificationListener() {
@@ -384,6 +393,10 @@
 
     private void handleFallback(StatusBarNotification sbn, String action,
             ActionRequestCallback callback) {
+        if (mFallbackAssistant == null) {
+            return;
+        }
+
         FallbackAssistant.Listener listener = new FallbackAssistant.Listener() {
             @Override
             public void onMessageRead(boolean hasError) {
diff --git a/car-media-common/res/values/bools.xml b/car-media-common/res/values/bools.xml
index 85fce20..fee9fec 100644
--- a/car-media-common/res/values/bools.xml
+++ b/car-media-common/res/values/bools.xml
@@ -22,4 +22,9 @@
     <!-- Whether to show a circular progress bar in control bar and minimized control bar or not. -->
     <bool name="show_circular_progress_bar">false</bool>
 
+    <!-- Whether the media source logo should be used for the app selector button. If the flag is
+        set to 'true', the main logo (which by default appears on the left hand side on toolbar)
+        will be hidden. -->
+    <bool name="use_media_source_logo_for_app_selector">false</bool>
+
 </resources>
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
index 60f4d18..d9ff4b1 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
@@ -51,8 +51,8 @@
 import com.android.car.apps.common.util.CarPackageManagerUtils;
 import com.android.car.apps.common.util.ViewUtils;
 import com.android.car.arch.common.FutureData;
-import com.android.car.media.common.browse.BrowsedMediaItems;
-import com.android.car.media.common.browse.MediaBrowserViewModel;
+import com.android.car.media.common.browse.MediaBrowserViewModelImpl;
+import com.android.car.media.common.browse.MediaItemsRepository;
 import com.android.car.media.common.playback.PlaybackViewModel;
 import com.android.car.media.common.playback.PlaybackViewModel.PlaybackStateWrapper;
 import com.android.car.media.common.source.MediaSource;
@@ -88,14 +88,14 @@
         mCar = Car.createCar(activity);
         mCarPackageManager = (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE);
 
-        mPlaybackViewModel = PlaybackViewModel.get(activity.getApplication(),
-                MEDIA_SOURCE_MODE_PLAYBACK);
-        mMediaSourceViewModel = MediaSourceViewModel.get(activity.getApplication(),
-                MEDIA_SOURCE_MODE_PLAYBACK);
+        Application application = activity.getApplication();
+        mPlaybackViewModel = PlaybackViewModel.get(application, MEDIA_SOURCE_MODE_PLAYBACK);
+        mMediaSourceViewModel = MediaSourceViewModel.get(application, MEDIA_SOURCE_MODE_PLAYBACK);
         mAppSelectorIntent = MediaSource.getSourceSelectorIntent(getContext(), true);
 
         mInnerViewModel = ViewModelProviders.of(activity).get(ViewModel.class);
-        mInnerViewModel.init(activity, mMediaSourceViewModel, mPlaybackViewModel);
+        mInnerViewModel.init(activity, mMediaSourceViewModel, mPlaybackViewModel,
+                MediaItemsRepository.get(application, MEDIA_SOURCE_MODE_PLAYBACK));
 
         View view = inflater.inflate(R.layout.playback_fragment, container, false);
 
@@ -121,8 +121,22 @@
         TextView subtitle = view.findViewById(R.id.subtitle);
         mInnerViewModel.getSubtitle().observe(getViewLifecycleOwner(), subtitle::setText);
 
+        boolean useSourceLogoForAppSelector =
+                getResources().getBoolean(R.bool.use_media_source_logo_for_app_selector);
+
         ImageView appIcon = view.findViewById(R.id.app_icon);
-        mInnerViewModel.getAppIcon().observe(getViewLifecycleOwner(), appIcon::setImageBitmap);
+        View appSelector = view.findViewById(R.id.app_selector_container);
+        mInnerViewModel.getAppIcon().observe(getViewLifecycleOwner(), bitmap -> {
+            if (useSourceLogoForAppSelector) {
+                ImageView appSelectorIcon = appSelector.findViewById(R.id.app_selector);
+                appSelectorIcon.setImageBitmap(bitmap);
+                appSelectorIcon.setImageTintList(null);
+
+                appIcon.setVisibility(View.GONE);
+            } else {
+                appIcon.setImageBitmap(bitmap);
+            }
+        });
 
         mInnerViewModel.getBrowseTreeHasChildren().observe(getViewLifecycleOwner(),
                 this::onBrowseTreeHasChildrenChanged);
@@ -152,11 +166,9 @@
         mPlaybackViewModel.getMetadata().observe(getViewLifecycleOwner(),
                 item -> mAlbumArtBinder.setImage(PlaybackFragment.this.getContext(),
                         item != null ? item.getArtworkKey() : null));
-        View appSelector = view.findViewById(R.id.app_selector_container);
         appSelector.setVisibility(mAppSelectorIntent != null ? View.VISIBLE : View.GONE);
         appSelector.setOnClickListener(e -> getContext().startActivity(mAppSelectorIntent));
 
-
         mErrorsHelper = new PlaybackErrorsHelper(activity) {
 
             @Override
@@ -214,32 +226,32 @@
         private LiveData<CharSequence> mSubtitle;
         private MutableLiveData<Boolean> mBrowseTreeHasChildren = new MutableLiveData<>();
 
+        private MediaItemsRepository mMediaItemsRepository;
         private PlaybackViewModel mPlaybackViewModel;
         private MediaSourceViewModel mMediaSourceViewModel;
-        private MediaBrowserViewModel mRootMediaBrowserViewModel;
 
         public ViewModel(Application application) {
             super(application);
         }
 
         void init(FragmentActivity activity, MediaSourceViewModel mediaSourceViewModel,
-                PlaybackViewModel playbackViewModel) {
+                PlaybackViewModel playbackViewModel, MediaItemsRepository mediaItemsRepository) {
             if (mMediaSourceViewModel == mediaSourceViewModel
-                    && mPlaybackViewModel == playbackViewModel) {
+                    && mPlaybackViewModel == playbackViewModel
+                    && mMediaItemsRepository == mediaItemsRepository) {
                 return;
             }
             mPlaybackViewModel = playbackViewModel;
             mMediaSourceViewModel = mediaSourceViewModel;
+            mMediaItemsRepository = mediaItemsRepository;
             mMediaSource = mMediaSourceViewModel.getPrimaryMediaSource();
             mAppName = mapNonNull(mMediaSource, MediaSource::getDisplayName);
             mAppIcon = mapNonNull(mMediaSource, MediaSource::getCroppedPackageIcon);
             mTitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getTitle);
             mSubtitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getArtist);
 
-            mRootMediaBrowserViewModel = MediaBrowserViewModel.Factory.getInstanceForBrowseRoot(
-                    mMediaSourceViewModel, ViewModelProviders.of(activity));
-            mRootMediaBrowserViewModel.getBrowsedMediaItems()
-                    .observe(activity, this::onItemsUpdate);
+            mMediaItemsRepository.getRootMediaItems()
+                    .observe(activity, this::onRootMediaItemsUpdate);
         }
 
         LiveData<CharSequence> getAppName() {
@@ -262,14 +274,14 @@
             return mBrowseTreeHasChildren;
         }
 
-        private void onItemsUpdate(FutureData<List<MediaItemMetadata>> futureData) {
-            if (futureData.isLoading()) {
+        private void onRootMediaItemsUpdate(FutureData<List<MediaItemMetadata>> data) {
+            if (data.isLoading()) {
                 mBrowseTreeHasChildren.setValue(null);
                 return;
             }
 
             List<MediaItemMetadata> items =
-                    BrowsedMediaItems.filterItems(/*forRoot*/ true, futureData.getData());
+                    MediaBrowserViewModelImpl.filterItems(/*forRoot*/ true, data.getData());
 
             boolean browseTreeHasChildren = items != null && !items.isEmpty();
             mBrowseTreeHasChildren.setValue(browseTreeHasChildren);
diff --git a/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java b/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
deleted file mode 100644
index c9a5864..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media.common.browse;
-
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.v4.media.MediaBrowserCompat;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.LiveData;
-
-import com.android.car.media.common.MediaItemMetadata;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-/**
- * A LiveData that provides access the a MediaBrowser's children
- */
-
-public class BrowsedMediaItems extends LiveData<List<MediaItemMetadata>> {
-
-    /**
-     * Number of times we will retry obtaining the list of children of a certain node
-     */
-    private static final int CHILDREN_SUBSCRIPTION_RETRIES = 1;
-    /**
-     * Time between retries while trying to obtain the list of children of a certain node
-     */
-    private static final int CHILDREN_SUBSCRIPTION_RETRY_TIME_MS = 5000;
-
-    /** Whether to send an error after the last timeout. The subscription stays active regardless.*/
-    private static final boolean LAST_RETRY_TIMEOUT_SENDS_ERROR = false;
-
-    private final MediaBrowserCompat mBrowser;
-    private final String mParentId;
-    private final Handler mHandler = new Handler();
-
-    private ChildrenSubscription mSubscription;
-
-    BrowsedMediaItems(@NonNull MediaBrowserCompat mediaBrowser, @NonNull String parentId) {
-        mBrowser = mediaBrowser;
-        mParentId = parentId;
-    }
-
-    /**
-     * Filters the items that are valid for the root (tabs) or the current node. Returns null when
-     * the given list is null to preserve its error signal.
-     */
-    @Nullable
-    public static List<MediaItemMetadata> filterItems(boolean forRoot,
-            @Nullable List<MediaItemMetadata> items) {
-        if (items == null) return null;
-        Predicate<MediaItemMetadata> predicate = forRoot ? MediaItemMetadata::isBrowsable
-                : item -> (item.isPlayable() || item.isBrowsable());
-        return items.stream().filter(predicate).collect(Collectors.toList());
-    }
-
-    @Override
-    protected void onActive() {
-        super.onActive();
-        mSubscription = new ChildrenSubscription(mParentId);
-        mSubscription.start(CHILDREN_SUBSCRIPTION_RETRIES, CHILDREN_SUBSCRIPTION_RETRY_TIME_MS);
-    }
-
-    @Override
-    protected void onInactive() {
-        super.onInactive();
-        mSubscription.stop();
-        mSubscription = null;
-        mHandler.removeCallbacksAndMessages(null);
-    }
-
-    /**
-     * {@link MediaBrowserCompat.SubscriptionCallback} wrapper used to overcome the lack of a
-     * reliable method to obtain the initial list of children of a given node.
-     * <p>
-     * When some 3rd party apps go through configuration changes (i.e., in the case of user-switch),
-     * they leave subscriptions in an intermediate state where neither {@link
-     * MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded(String, List)} nor {@link
-     * MediaBrowserCompat.SubscriptionCallback#onError(String)} are invoked.
-     * <p>
-     * This wrapper works around this problem by retrying the subscription a given number of times
-     * if no data is received after a certain amount of time. This process is started by calling
-     * {@link #start(int, int)}, passing the number of retries and delay between them as
-     * parameters.
-     * TODO: remove all this code if it's indeed not needed anymore (using retry=1 to be sure).
-     */
-    private class ChildrenSubscription extends MediaBrowserCompat.SubscriptionCallback {
-        private final String mItemId;
-
-        private boolean mIsDataLoaded;
-        private int mRetries;
-        private int mRetryDelay;
-
-        ChildrenSubscription(String itemId) {
-            mItemId = itemId;
-        }
-
-        private Runnable mRetryRunnable = new Runnable() {
-            @Override
-            public void run() {
-                if (!mIsDataLoaded) {
-                    if (mRetries > 0) {
-                        mRetries--;
-                        mBrowser.unsubscribe(mItemId);
-                        mBrowser.subscribe(mItemId, ChildrenSubscription.this);
-                        mHandler.postDelayed(this, mRetryDelay);
-                    } else if (LAST_RETRY_TIMEOUT_SENDS_ERROR) {
-                        mIsDataLoaded = true;
-                        setValue(null);
-                    }
-                }
-            }
-        };
-
-        /**
-         * Starts trying to obtain the list of children
-         *
-         * @param retries    number of times to retry. If children are not obtained in this time
-         *                   then the LiveData's value will be set to {@code null}
-         * @param retryDelay time between retries in milliseconds
-         */
-        void start(int retries, int retryDelay) {
-            if (mIsDataLoaded) {
-                mBrowser.subscribe(mItemId, this);
-            } else {
-                mRetries = retries;
-                mRetryDelay = retryDelay;
-                mHandler.post(mRetryRunnable);
-            }
-        }
-
-        /**
-         * Stops retrying
-         */
-        void stop() {
-            mHandler.removeCallbacks(mRetryRunnable);
-            mBrowser.unsubscribe(mItemId);
-        }
-
-        @Override
-        public void onChildrenLoaded(@NonNull String parentId,
-                @NonNull List<MediaBrowserCompat.MediaItem> children) {
-            mHandler.removeCallbacks(mRetryRunnable);
-            mIsDataLoaded = true;
-            setValue(children.stream()
-                    .filter(Objects::nonNull)
-                    .map(MediaItemMetadata::new)
-                    .collect(Collectors.toList()));
-        }
-
-        @Override
-        public void onChildrenLoaded(@NonNull String parentId,
-                @NonNull List<MediaBrowserCompat.MediaItem> children,
-                @NonNull Bundle options) {
-            onChildrenLoaded(parentId, children);
-        }
-
-        @Override
-        public void onError(@NonNull String parentId) {
-            mHandler.removeCallbacks(mRetryRunnable);
-            mIsDataLoaded = true;
-            setValue(null);
-        }
-
-        @Override
-        public void onError(@NonNull String parentId, @NonNull Bundle options) {
-            onError(parentId);
-        }
-    }
-}
diff --git a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java
deleted file mode 100644
index 323b4ce..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media.common.browse;
-
-import android.support.v4.media.MediaBrowserCompat;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.ViewModelProvider;
-
-import com.android.car.arch.common.FutureData;
-import com.android.car.media.common.MediaItemMetadata;
-import com.android.car.media.common.source.MediaSourceViewModel;
-
-import java.util.List;
-
-/**
- * Contains observable data needed for displaying playback and browse UI. Instances can be obtained
- * via {@link MediaBrowserViewModel.Factory}
- */
-public interface MediaBrowserViewModel {
-
-    /**
-     * Returns a LiveData that emits the current package name of the browser's service component.
-     */
-    LiveData<String> getPackageName();
-
-    /**
-     * Fetches the MediaItemMetadatas for the current browsed id, and the loading status of the
-     * fetch operation.
-     *
-     * This LiveData will never emit {@code null}. If the data is loading, the data component of the
-     * {@link FutureData} will be null
-     * A MediaSource must be selected and its MediaBrowser connected, otherwise the FutureData will
-     * always contain a {@code null} data value.
-     *
-     * @return a LiveData that emits a FutureData that contains the loading status and the
-     * MediaItemMetadatas for the current browsed id
-     */
-    LiveData<FutureData<List<MediaItemMetadata>>> getBrowsedMediaItems();
-
-    /**
-     * Fetches the MediaItemMetadatas for the current search query, and the loading status of the
-     * fetch operation.
-     *
-     * See {@link #getBrowsedMediaItems()}
-     */
-    LiveData<FutureData<List<MediaItemMetadata>>> getSearchedMediaItems();
-
-
-    /**
-     * Returns a LiveData that emits whether the media browser supports search. This wil never emit
-     * {@code null}
-     */
-    LiveData<Boolean> supportsSearch();
-
-    /**
-     * Gets the content style display type of browsable elements in this media browser, set at the
-     * browse root
-     */
-    LiveData<Integer> rootBrowsableHint();
-
-    /**
-     * Gets the content style display type of playable elements in this media browser, set at the
-     * browse root
-     */
-    LiveData<Integer> rootPlayableHint();
-
-    /**
-     * A {@link MediaBrowserViewModel} whose selected browse ID may be changed.
-     */
-    interface WithMutableBrowseId extends MediaBrowserViewModel {
-
-        /**
-         * Set the current item to be browsed. If available, the list of items will be emitted by
-         * {@link #getBrowsedMediaItems()}.
-         */
-        @UiThread
-        void setCurrentBrowseId(@NonNull String browseId);
-
-        /**
-         * Set the current item to be searched for. If available, the list of items will be emitted
-         * by {@link #getBrowsedMediaItems()}.
-         */
-        @UiThread
-        void search(@Nullable String query);
-    }
-
-    /**
-     * Creates and/or fetches {@link MediaBrowserViewModel} instances.
-     */
-    class Factory {
-
-        private static final String KEY_BROWSER_ROOT =
-                "com.android.car.media.common.browse.MediaBrowserViewModel.Factory.browserRoot";
-
-        /**
-         * Returns an initialized {@link MediaBrowserViewModel.WithMutableBrowseId} with the
-         * provided connected media browser. The provided {@code mediaBrowser} does not need to be
-         * from the same scope as {@code viewModelProvider}.
-         */
-        @NonNull
-        public static MediaBrowserViewModel.WithMutableBrowseId getInstanceWithMediaBrowser(
-                @NonNull String key,
-                @NonNull ViewModelProvider viewModelProvider,
-                @NonNull LiveData<MediaBrowserCompat> mediaBrowser) {
-            MutableMediaBrowserViewModel viewModel =
-                    viewModelProvider.get(key, MutableMediaBrowserViewModel.class);
-            initMediaBrowser(mediaBrowser, viewModel);
-            return viewModel;
-        }
-
-        /**
-         * Fetch an initialized {@link MediaBrowserViewModel}. It will get its media browser from
-         * the {@link MediaSourceViewModel} provided by {@code viewModelProvider}. It will already
-         * be configured to browse the root of the browser.
-         *
-         * @param mediaSourceVM     the {@link MediaSourceViewModel} singleton.
-         * @param viewModelProvider the ViewModelProvider to load ViewModels from.
-         * @return an initialized MediaBrowserViewModel configured to browse the specified browseId.
-         */
-        @NonNull
-        public static MediaBrowserViewModel getInstanceForBrowseRoot(
-                MediaSourceViewModel mediaSourceVM, @NonNull ViewModelProvider viewModelProvider) {
-            RootMediaBrowserViewModel viewModel =
-                    viewModelProvider.get(KEY_BROWSER_ROOT, RootMediaBrowserViewModel.class);
-            initMediaBrowser(mediaSourceVM.getConnectedMediaBrowser(), viewModel);
-            return viewModel;
-        }
-
-        private static void initMediaBrowser(
-                @NonNull LiveData<MediaBrowserCompat> connectedMediaBrowser,
-                MediaBrowserViewModelImpl viewModel) {
-            if (viewModel.getMediaBrowserSource() != connectedMediaBrowser) {
-                viewModel.setConnectedMediaBrowser(connectedMediaBrowser);
-            }
-        }
-    }
-}
diff --git a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java
index 949c81d..d740667 100644
--- a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java
+++ b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java
@@ -16,177 +16,124 @@
 
 package com.android.car.media.common.browse;
 
-import static androidx.lifecycle.Transformations.map;
-
-import static com.android.car.arch.common.LiveDataFunctions.dataOf;
-import static com.android.car.arch.common.LiveDataFunctions.loadingSwitchMap;
-import static com.android.car.arch.common.LiveDataFunctions.pair;
-import static com.android.car.arch.common.LiveDataFunctions.split;
-
-import android.app.Application;
 import android.os.Bundle;
 import android.support.v4.media.MediaBrowserCompat;
-import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
 
-import com.android.car.arch.common.FutureData;
-import com.android.car.arch.common.switching.SwitchingLiveData;
 import com.android.car.media.common.MediaConstants;
 import com.android.car.media.common.MediaItemMetadata;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
- * Contains observable data needed for displaying playback and browse/search UI. Instances can be
- * obtained via {@link MediaBrowserViewModel.Factory}
+ * TODO: rename to MediaBrowserUtils.
+ * Provides utility methods for {@link MediaBrowserCompat}.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-class MediaBrowserViewModelImpl extends AndroidViewModel implements MediaBrowserViewModel {
+public class MediaBrowserViewModelImpl {
 
-    private final boolean mIsRoot;
-
-    private final SwitchingLiveData<MediaBrowserCompat> mMediaBrowserSwitch =
-            SwitchingLiveData.newInstance();
-
-    final MutableLiveData<String> mCurrentBrowseId = dataOf(null);
-    final MutableLiveData<String> mCurrentSearchQuery = dataOf(null);
-    private final LiveData<MediaBrowserCompat> mConnectedMediaBrowser =
-            map(mMediaBrowserSwitch.asLiveData(), MediaBrowserViewModelImpl::requireConnected);
-
-    private final LiveData<FutureData<List<MediaItemMetadata>>> mSearchedMediaItems;
-    private final LiveData<FutureData<List<MediaItemMetadata>>> mBrowsedMediaItems;
-    private final LiveData<String> mPackageName;
-
-    MediaBrowserViewModelImpl(@NonNull Application application, boolean isRoot) {
-        super(application);
-
-        mIsRoot = isRoot;
-
-        mPackageName = map(mConnectedMediaBrowser,
-                mediaBrowser -> {
-                    if (mediaBrowser == null) return null;
-                    return mediaBrowser.getServiceComponent().getPackageName();
-                });
-
-        mBrowsedMediaItems =
-                loadingSwitchMap(pair(mConnectedMediaBrowser, mCurrentBrowseId),
-                        split((mediaBrowser, browseId) -> {
-                            if (mediaBrowser == null || (!mIsRoot && browseId == null)) {
-                                return null;
-                            }
-
-                            String parentId = (mIsRoot) ? mediaBrowser.getRoot() : browseId;
-                            return new BrowsedMediaItems(mediaBrowser, parentId);
-                        }));
-        mSearchedMediaItems =
-                loadingSwitchMap(pair(mConnectedMediaBrowser, mCurrentSearchQuery),
-                        split((mediaBrowser, query) ->
-                                (mediaBrowser == null || TextUtils.isEmpty(query))
-                                        ? null
-                                        : new SearchedMediaItems(mediaBrowser, query)));
-    }
-
-    private static MediaBrowserCompat requireConnected(@Nullable MediaBrowserCompat mediaBrowser) {
-        if (mediaBrowser != null && !mediaBrowser.isConnected()) {
-            throw new IllegalStateException(
-                    "Only connected MediaBrowsers may be provided to MediaBrowserViewModel.");
-        }
-        return mediaBrowser;
+    private MediaBrowserViewModelImpl() {
     }
 
     /**
-     * Set the source {@link MediaBrowserCompat} to use for browsing. If {@code mediaBrowser} emits
-     * non-null, the MediaBrowser emitted must already be in a connected state.
+     * Filters the items that are valid for the root (tabs) or the current node. Returns null when
+     * the given list is null to preserve its error signal.
      */
-    void setConnectedMediaBrowser(@Nullable LiveData<MediaBrowserCompat> mediaBrowser) {
-        mMediaBrowserSwitch.setSource(mediaBrowser);
+    @Nullable
+    public static List<MediaItemMetadata> filterItems(boolean forRoot,
+            @Nullable List<MediaItemMetadata> items) {
+        if (items == null) return null;
+        Predicate<MediaItemMetadata> predicate = forRoot ? MediaItemMetadata::isBrowsable
+                : item -> (item.isPlayable() || item.isBrowsable());
+        return items.stream().filter(predicate).collect(Collectors.toList());
     }
 
-    LiveData<? extends MediaBrowserCompat> getMediaBrowserSource() {
-        return mMediaBrowserSwitch.getSource();
+    /** Returns only the browse-able items from the given list. */
+    @Nullable
+    public static List<MediaItemMetadata> selectBrowseableItems(
+            @Nullable List<MediaItemMetadata> items) {
+        if (items == null) return null;
+        Predicate<MediaItemMetadata> predicate = MediaItemMetadata::isBrowsable;
+        return items.stream().filter(predicate).collect(Collectors.toList());
     }
 
-    @Override
-    public LiveData<String> getPackageName() {
-        return mPackageName;
-    }
-
-    @Override
-    public LiveData<FutureData<List<MediaItemMetadata>>> getBrowsedMediaItems() {
-        return mBrowsedMediaItems;
-    }
-
-    @Override
-    public LiveData<FutureData<List<MediaItemMetadata>>> getSearchedMediaItems() {
-        return mSearchedMediaItems;
-    }
 
     @SuppressWarnings("deprecation")
-    @Override
-    public LiveData<Boolean> supportsSearch() {
-        return map(mConnectedMediaBrowser, mediaBrowserCompat -> {
-            if (mediaBrowserCompat == null) {
-                return false;
-            }
-            Bundle extras = mediaBrowserCompat.getExtras();
-            if (extras == null) {
-                return false;
-            }
-            if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED)) {
-                return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED);
-            }
-            if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE)) {
-                return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE);
-            }
+    public static boolean getSupportsSearch(@Nullable MediaBrowserCompat mediaBrowserCompat) {
+        if (mediaBrowserCompat == null) {
             return false;
-        });
+        }
+        Bundle extras = mediaBrowserCompat.getExtras();
+        if (extras == null) {
+            return false;
+        }
+        if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED)) {
+            return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED);
+        }
+        if (extras.containsKey(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE)) {
+            return extras.getBoolean(MediaConstants.MEDIA_SEARCH_SUPPORTED_PRERELEASE);
+        }
+        return false;
     }
 
     @SuppressWarnings("deprecation")
-    @Override
-    public LiveData<Integer> rootBrowsableHint() {
-        return map(mConnectedMediaBrowser, mediaBrowserCompat -> {
-            if (mediaBrowserCompat == null) {
-                return 0;
-            }
-            Bundle extras = mediaBrowserCompat.getExtras();
-            if (extras == null) {
-                return 0;
-            }
-            if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT)) {
-                return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT, 0);
-            }
-            if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE)) {
-                return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE, 0);
-            }
+    public static int getRootBrowsableHint(@Nullable MediaBrowserCompat mediaBrowserCompat) {
+        if (mediaBrowserCompat == null) {
             return 0;
-        });
+        }
+        Bundle extras = mediaBrowserCompat.getExtras();
+        if (extras == null) {
+            return 0;
+        }
+        if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT)) {
+            return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT, 0);
+        }
+        if (extras.containsKey(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE)) {
+            return extras.getInt(MediaConstants.CONTENT_STYLE_BROWSABLE_HINT_PRERELEASE, 0);
+        }
+        return 0;
     }
 
     @SuppressWarnings("deprecation")
-    @Override
-    public LiveData<Integer> rootPlayableHint() {
-        return map(mConnectedMediaBrowser, mediaBrowserCompat -> {
-            if (mediaBrowserCompat == null) {
-                return 0;
-            }
-            Bundle extras = mediaBrowserCompat.getExtras();
-            if (extras == null) {
-                return 0;
-            }
-            if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT)) {
-                return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT, 0);
-            }
-            if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE)) {
-                return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE, 0);
-            }
+    public static int getRootPlayableHint(@Nullable MediaBrowserCompat mediaBrowserCompat) {
+        if (mediaBrowserCompat == null) {
             return 0;
-        });
+        }
+        Bundle extras = mediaBrowserCompat.getExtras();
+        if (extras == null) {
+            return 0;
+        }
+        if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT)) {
+            return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT, 0);
+        }
+        if (extras.containsKey(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE)) {
+            return extras.getInt(MediaConstants.CONTENT_STYLE_PLAYABLE_HINT_PRERELEASE, 0);
+        }
+        return 0;
+    }
+
+    /** Returns the elements of oldList that do NOT appear in newList. */
+    public static @NonNull Collection<MediaItemMetadata> computeRemovedItems(
+            @Nullable List<MediaItemMetadata> oldList, @Nullable List<MediaItemMetadata> newList) {
+        if (oldList == null || oldList.isEmpty()) {
+            // Nothing was removed
+            return Collections.emptyList();
+        }
+
+        if (newList == null || newList.isEmpty()) {
+            // Everything was removed
+            return new ArrayList<>(oldList);
+        }
+
+        HashSet<MediaItemMetadata> itemsById = new HashSet<>(oldList);
+        itemsById.removeAll(newList);
+        return itemsById;
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/browse/MediaItemsRepository.java b/car-media-common/src/com/android/car/media/common/browse/MediaItemsRepository.java
new file mode 100644
index 0000000..e01cb96
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/browse/MediaItemsRepository.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.media.common.browse;
+
+import static com.android.car.arch.common.LiveDataFunctions.dataOf;
+
+import static java.util.stream.Collectors.toList;
+
+import android.app.Application;
+import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaBrowserCompat.SearchCallback;
+import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.arch.common.FutureData;
+import com.android.car.media.common.MediaItemMetadata;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+import com.android.car.media.common.source.MediaSource;
+import com.android.car.media.common.source.MediaSourceViewModel;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+
+/**
+ * Fulfills media items search and children queries. The latter also provides the last list of
+ * results alongside the new list so that differences can be calculated and acted upon.
+ */
+public class MediaItemsRepository {
+    private static final String TAG = "MediaItemsRepository";
+
+    /** One instance per MEDIA_SOURCE_MODE. */
+    private static MediaItemsRepository[] sInstances = new MediaItemsRepository[2];
+
+    /** Returns the MediaItemsRepository "singleton" tied to the application for the given mode. */
+    public static MediaItemsRepository get(@NonNull Application application, int mode) {
+        if (sInstances[mode] == null) {
+            sInstances[mode] = new MediaItemsRepository(
+                    MediaSourceViewModel.get(application, mode).getBrowsingState());
+        }
+        return sInstances[mode];
+    }
+
+    /** The live data providing the updates for a query. */
+    public static class MediaItemsLiveData
+            extends LiveData<FutureData<List<MediaItemMetadata>>> {
+
+        private MediaItemsLiveData() {
+            this(true);
+        }
+
+        private MediaItemsLiveData(boolean initAsLoading) {
+            if (initAsLoading) {
+                setLoading();
+            } else {
+                clear();
+            }
+        }
+
+        private void onDataLoaded(List<MediaItemMetadata> old, List<MediaItemMetadata> list) {
+            setValue(FutureData.newLoadedData(old, list));
+        }
+
+        private void setLoading() {
+            setValue(FutureData.newLoadingData());
+        }
+
+        private void clear() {
+            setValue(null);
+        }
+    }
+
+    private static class MediaChildren {
+        final String mNodeId;
+        final MediaItemsLiveData mLiveData = new MediaItemsLiveData();
+        List<MediaItemMetadata> mPreviousValue = Collections.emptyList();
+
+        MediaChildren(String nodeId) {
+            mNodeId = nodeId;
+        }
+    }
+
+    private static class PerMediaSourceCache {
+        String mRootId;
+        Map<String, MediaChildren> mChildrenByNodeId = new HashMap<>();
+    }
+
+    private BrowsingState mBrowsingState;
+    private final Map<MediaSource, PerMediaSourceCache> mCaches = new HashMap<>();
+    private final MutableLiveData<BrowsingState> mBrowsingStateLiveData = dataOf(null);
+    private final MediaItemsLiveData mRootMediaItems = new MediaItemsLiveData();
+    private final MediaItemsLiveData mSearchMediaItems = new MediaItemsLiveData(/*loading*/ false);
+
+    private String mSearchQuery;
+
+    @VisibleForTesting
+    public MediaItemsRepository(LiveData<BrowsingState> browsingState) {
+        browsingState.observeForever(this::onMediaBrowsingStateChanged);
+    }
+
+    /**
+     * Rebroadcasts browsing state changes before the repository takes any action on them.
+     */
+    public LiveData<BrowsingState> getBrowsingState() {
+        return mBrowsingStateLiveData;
+    }
+
+    /**
+     * Convenience wrapper for root media items. The live data is the same instance for all
+     * media sources.
+     */
+    public MediaItemsLiveData getRootMediaItems() {
+        return mRootMediaItems;
+    }
+
+    /**
+     * Returns the results from the current search query. The live data is the same instance
+     * for all media sources.
+     */
+    public MediaItemsLiveData getSearchMediaItems() {
+        return mSearchMediaItems;
+    }
+
+    /** Returns the children of the given node. */
+    public MediaItemsLiveData getMediaChildren(String nodeId) {
+        PerMediaSourceCache cache = getCache();
+        MediaChildren items = cache.mChildrenByNodeId.get(nodeId);
+        if (items == null) {
+            items = new MediaChildren(nodeId);
+            cache.mChildrenByNodeId.put(nodeId, items);
+        }
+
+        // Always refresh the subscription (to work around bugs in media apps).
+        mBrowsingState.mBrowser.unsubscribe(nodeId);
+        mBrowsingState.mBrowser.subscribe(nodeId, mBrowseCallback);
+
+        return items.mLiveData;
+    }
+
+    /** Sets the search query. Results will be given through {@link #getSearchMediaItems}. */
+    public void setSearchQuery(String query) {
+        mSearchQuery = query;
+        if (TextUtils.isEmpty(mSearchQuery)) {
+            clearSearchResults();
+        } else {
+            mSearchMediaItems.setLoading();
+            mBrowsingState.mBrowser.search(mSearchQuery, null, mSearchCallback);
+        }
+    }
+
+    private void clearSearchResults() {
+        mSearchMediaItems.clear();
+    }
+
+    private MediaSource getMediaSource() {
+        return (mBrowsingState != null) ? mBrowsingState.mMediaSource : null;
+    }
+
+    private void onMediaBrowsingStateChanged(BrowsingState newBrowsingState) {
+        mBrowsingState = newBrowsingState;
+        if (mBrowsingState == null) {
+            Log.e(TAG, "Null browsing state (no media source!)");
+            return;
+        }
+        mBrowsingStateLiveData.setValue(mBrowsingState);
+        switch (mBrowsingState.mConnectionStatus) {
+            case CONNECTING:
+                mRootMediaItems.setLoading();
+                break;
+            case CONNECTED:
+                String rootId = mBrowsingState.mBrowser.getRoot();
+                getCache().mRootId = rootId;
+                getMediaChildren(rootId);
+                break;
+            case DISCONNECTING:
+                unsubscribeNodes();
+                clearSearchResults();
+                clearNodes();
+                break;
+            case REJECTED:
+            case SUSPENDED:
+                onBrowseData(getCache().mRootId, null);
+                clearSearchResults();
+                clearNodes();
+        }
+    }
+
+    private PerMediaSourceCache getCache() {
+        PerMediaSourceCache cache = mCaches.get(getMediaSource());
+        if (cache == null) {
+            cache = new PerMediaSourceCache();
+            mCaches.put(getMediaSource(), cache);
+        }
+        return cache;
+    }
+
+    /** Does NOT clear the cache. */
+    private void unsubscribeNodes() {
+        PerMediaSourceCache cache = getCache();
+        for (String nodeId : cache.mChildrenByNodeId.keySet()) {
+            mBrowsingState.mBrowser.unsubscribe(nodeId);
+        }
+    }
+
+    /** Does NOT unsubscribe nodes. */
+    private void clearNodes() {
+        PerMediaSourceCache cache = getCache();
+        cache.mChildrenByNodeId.clear();
+    }
+
+    private void onBrowseData(@NonNull String parentId, @Nullable List<MediaItemMetadata> list) {
+        PerMediaSourceCache cache = getCache();
+        MediaChildren children = cache.mChildrenByNodeId.get(parentId);
+        if (children == null) {
+            if (Log.isLoggable(TAG, Log.WARN)) {
+                Log.w(TAG, "Browse parent not in the cache: " + parentId);
+            }
+            return;
+        }
+
+        List<MediaItemMetadata> old = children.mPreviousValue;
+        children.mPreviousValue = list;
+        children.mLiveData.onDataLoaded(old, list);
+
+        if (Objects.equals(parentId, cache.mRootId)) {
+            mRootMediaItems.onDataLoaded(old, list);
+        }
+    }
+
+    private void onSearchData(@Nullable List<MediaItemMetadata> list) {
+        mSearchMediaItems.onDataLoaded(null, list);
+    }
+
+    private final SubscriptionCallback mBrowseCallback = new SubscriptionCallback() {
+        @Override
+        public void onChildrenLoaded(@NonNull String parentId,
+                @NonNull List<MediaBrowserCompat.MediaItem> children) {
+
+            onBrowseData(parentId, children.stream()
+                    .filter(Objects::nonNull)
+                    .map(MediaItemMetadata::new)
+                    .collect(Collectors.toList()));
+        }
+
+        @Override
+        public void onChildrenLoaded(@NonNull String parentId,
+                @NonNull List<MediaBrowserCompat.MediaItem> children,
+                @NonNull Bundle options) {
+            onChildrenLoaded(parentId, children);
+        }
+
+        @Override
+        public void onError(@NonNull String parentId) {
+            onBrowseData(parentId, null);
+        }
+
+        @Override
+        public void onError(@NonNull String parentId, @NonNull Bundle options) {
+            onError(parentId);
+        }
+    };
+
+    private final SearchCallback mSearchCallback = new SearchCallback() {
+        @Override
+        public void onSearchResult(@NonNull String query, Bundle extras,
+                @NonNull List<MediaBrowserCompat.MediaItem> items) {
+            super.onSearchResult(query, extras, items);
+            if (Objects.equals(mSearchQuery, query)) {
+                onSearchData(items.stream()
+                        .filter(Objects::nonNull)
+                        .map(MediaItemMetadata::new)
+                        .collect(toList()));
+            }
+        }
+
+        @Override
+        public void onError(@NonNull String query, Bundle extras) {
+            super.onError(query, extras);
+            if (Objects.equals(mSearchQuery, query)) {
+                onSearchData(null);
+            }
+        }
+    };
+}
diff --git a/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java b/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java
deleted file mode 100644
index 1290b20..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media.common.browse;
-
-import android.app.Application;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.UiThread;
-
-/** This isn't a comment. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class MutableMediaBrowserViewModel extends MediaBrowserViewModelImpl implements
-        MediaBrowserViewModel.WithMutableBrowseId {
-    public MutableMediaBrowserViewModel(@NonNull Application application) {
-        super(application, /*isRoot*/ false);
-    }
-
-    @UiThread
-    @Override
-    public void setCurrentBrowseId(@NonNull String browseId) {
-        super.mCurrentBrowseId.setValue(browseId);
-    }
-
-    @UiThread
-    @Override
-    public void search(@Nullable String query) {
-        super.mCurrentSearchQuery.setValue(query);
-    }
-}
diff --git a/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java b/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java
deleted file mode 100644
index f1ffc64..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media.common.browse;
-
-import android.app.Application;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-/** This isn't a comment. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class RootMediaBrowserViewModel extends MediaBrowserViewModelImpl {
-    public RootMediaBrowserViewModel(@NonNull Application application) {
-        super(application, /*isRoot*/ true);
-    }
-}
diff --git a/car-media-common/src/com/android/car/media/common/browse/SearchedMediaItems.java b/car-media-common/src/com/android/car/media/common/browse/SearchedMediaItems.java
deleted file mode 100644
index b8c7423..0000000
--- a/car-media-common/src/com/android/car/media/common/browse/SearchedMediaItems.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media.common.browse;
-
-import static java.util.stream.Collectors.toList;
-
-import android.os.Bundle;
-import android.support.v4.media.MediaBrowserCompat;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.LiveData;
-
-import com.android.car.media.common.MediaItemMetadata;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A LiveData that provides access to a MediaBrowser's search results for a given query
- */
-public class SearchedMediaItems extends LiveData<List<MediaItemMetadata>> {
-
-    private final MediaBrowserCompat mBrowser;
-    private final String mQuery;
-
-    private final MediaBrowserCompat.SearchCallback mCallback =
-            new MediaBrowserCompat.SearchCallback() {
-        @Override
-        public void onSearchResult(@NonNull String query, Bundle extras,
-                                   @NonNull List<MediaBrowserCompat.MediaItem> items) {
-            super.onSearchResult(query, extras, items);
-            setValue(items.stream()
-                    .filter(Objects::nonNull)
-                    .map(MediaItemMetadata::new)
-                    .collect(toList()));
-        }
-
-        @Override
-        public void onError(@NonNull String query, Bundle extras) {
-            super.onError(query, extras);
-            setValue(null);
-        }
-    };
-
-    SearchedMediaItems(@NonNull MediaBrowserCompat mediaBrowser, @Nullable String query) {
-        mBrowser = mediaBrowser;
-        mQuery = query;
-    }
-
-    @Override
-    protected void onActive() {
-        super.onActive();
-        mBrowser.search(mQuery, null, mCallback);
-    }
-}
diff --git a/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java b/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
index bcb9893..713caa1 100644
--- a/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
+++ b/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
@@ -16,8 +16,6 @@
 
 package com.android.car.media.common.playback;
 
-import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
-
 import static androidx.lifecycle.Transformations.switchMap;
 
 import static com.android.car.arch.common.LiveDataFunctions.dataOf;
@@ -30,6 +28,7 @@
 import android.graphics.drawable.Drawable;
 import android.media.MediaMetadata;
 import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat;
 import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.RatingCompat;
 import android.support.v4.media.session.MediaControllerCompat;
@@ -50,6 +49,8 @@
 import com.android.car.media.common.MediaConstants;
 import com.android.car.media.common.MediaItemMetadata;
 import com.android.car.media.common.R;
+import com.android.car.media.common.source.MediaBrowserConnector;
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionStatus;
 import com.android.car.media.common.source.MediaSourceColors;
 import com.android.car.media.common.source.MediaSourceViewModel;
 
@@ -67,7 +68,7 @@
  * Observes changes to the provided MediaController to expose playback state and metadata
  * observables.
  * <p>
- * PlaybackViewModel is a singleton tied to the application to provide a single source of truth.
+ * PlaybackViewModel is a "singleton" tied to the application to provide a single source of truth.
  */
 public class PlaybackViewModel extends AndroidViewModel {
     private static final String TAG = "PlaybackViewModel";
@@ -78,15 +79,7 @@
 
     private static PlaybackViewModel[] sInstances = new PlaybackViewModel[2];
 
-    /**
-     * Returns the PlaybackViewModel singleton tied to the application.
-     * @deprecated should use get(Application application, int mode) instead
-     */
-    public static PlaybackViewModel get(@NonNull Application application) {
-        return get(application, MEDIA_SOURCE_MODE_PLAYBACK);
-    }
-
-    /** Returns the PlaybackViewModel singleton tied to the application. */
+    /** Returns the PlaybackViewModel "singleton" tied to the application for the given mode. */
     public static PlaybackViewModel get(@NonNull Application application, int mode) {
         if (sInstances[mode] == null) {
             sInstances[mode] = new PlaybackViewModel(application, mode);
@@ -119,12 +112,21 @@
      */
     public static final int ACTION_PAUSE = 3;
 
+    /**
+     * Factory for creating dependencies. Can be swapped out for testing.
+     */
+    @VisibleForTesting
+    interface InputFactory {
+        MediaControllerCompat getControllerForBrowser(@NonNull MediaBrowserCompat browser);
+    }
+
+
     /** Needs to be a MediaMetadata because the compat class doesn't implement equals... */
     private static final MediaMetadata EMPTY_MEDIA_METADATA = new MediaMetadata.Builder().build();
 
     private final MediaControllerCallback mMediaControllerCallback = new MediaControllerCallback();
-    private final Observer<MediaControllerCompat> mMediaControllerObserver =
-            mMediaControllerCallback::onMediaControllerChanged;
+    private final Observer<MediaBrowserConnector.BrowsingState> mMediaBrowsingObserver =
+            mMediaControllerCallback::onMediaBrowsingStateChanged;
 
     private final MediaSourceColors.Factory mColorsFactory;
     private final MutableLiveData<MediaSourceColors> mColors = dataOf(null);
@@ -147,15 +149,20 @@
                     state -> state == null ? dataOf(new PlaybackProgress(0L, 0L))
                             : new ProgressLiveData(state.mState, state.getMaxProgress()));
 
+    private final InputFactory mInputFactory;
+
     private PlaybackViewModel(Application application, int mode) {
-        this(application, MediaSourceViewModel.get(application, mode).getMediaController());
+        this(application, MediaSourceViewModel.get(application, mode).getBrowsingState(),
+                browser -> new MediaControllerCompat(application, browser.getSessionToken()));
     }
 
     @VisibleForTesting
-    public PlaybackViewModel(Application application, LiveData<MediaControllerCompat> controller) {
+    public PlaybackViewModel(Application application,
+            LiveData<MediaBrowserConnector.BrowsingState> browsingState, InputFactory factory) {
         super(application);
+        mInputFactory =  factory;
         mColorsFactory = new MediaSourceColors.Factory(application);
-        controller.observeForever(mMediaControllerObserver);
+        browsingState.observeForever(mMediaBrowsingObserver);
     }
 
     /**
@@ -231,29 +238,50 @@
 
     private class MediaControllerCallback extends MediaControllerCompat.Callback {
 
+        private MediaBrowserConnector.BrowsingState mBrowsingState;
         private MediaControllerCompat mMediaController;
         private MediaMetadataCompat mMediaMetadata;
         private PlaybackStateCompat mPlaybackState;
 
-        void onMediaControllerChanged(MediaControllerCompat controller) {
-            if (mMediaController == controller) {
-                Log.w(TAG, "onMediaControllerChanged noop");
+
+        void onMediaBrowsingStateChanged(MediaBrowserConnector.BrowsingState newBrowsingState) {
+            if (Objects.equals(mBrowsingState, newBrowsingState)) {
+                Log.w(TAG, "onMediaBrowsingStateChanged noop ");
                 return;
             }
 
+            // Reset the old controller if any, unregistering the callback when browsing is
+            // not suspended (crashed).
             if (mMediaController != null) {
-                mMediaController.unregisterCallback(this);
+                switch (newBrowsingState.mConnectionStatus) {
+                    case DISCONNECTING:
+                    case REJECTED:
+                    case CONNECTING:
+                    case CONNECTED:
+                        mMediaController.unregisterCallback(this);
+                        // Fall through
+                    case SUSPENDED:
+                        setMediaController(null);
+                }
             }
 
+            mBrowsingState = newBrowsingState;
+
+            if (mBrowsingState.mConnectionStatus == ConnectionStatus.CONNECTED) {
+                setMediaController(mInputFactory.getControllerForBrowser(mBrowsingState.mBrowser));
+            }
+        }
+
+        private void setMediaController(MediaControllerCompat mediaController) {
             mMediaMetadata = null;
             mPlaybackState = null;
-            mMediaController = controller;
-            mPlaybackControls.setValue(new PlaybackController(controller));
+            mMediaController = mediaController;
+            mPlaybackControls.setValue(new PlaybackController(mediaController));
 
             if (mMediaController != null) {
                 mMediaController.registerCallback(this);
 
-                mColors.setValue(mColorsFactory.extractColors(controller.getPackageName()));
+                mColors.setValue(mColorsFactory.extractColors(mediaController.getPackageName()));
 
                 // The apps don't always send updates so make sure we fetch the most recent values.
                 onMetadataChanged(mMediaController.getMetadata());
@@ -274,7 +302,9 @@
         @Override
         public void onSessionDestroyed() {
             Log.w(TAG, "onSessionDestroyed");
-            onMediaControllerChanged(null);
+            // Bypass the unregisterCallback as the controller is dead.
+            // TODO: consider keeping track of orphaned callbacks in case they are resurrected...
+            setMediaController(null);
         }
 
         @Override
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java b/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
index 14271e4..c1ef1d9 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
@@ -26,30 +26,106 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.util.Preconditions;
 
 import com.android.car.media.common.MediaConstants;
 
+import java.util.Objects;
+
 /**
- * A helper class to connect to a single {@link MediaBrowserCompat}. Connecting to a new one
- * automatically disconnects the previous browser. Changes of the currently connected browser are
- * sent via {@link MediaBrowserConnector.Callback}.
+ * A helper class to connect to a single {@link MediaSource} to its {@link MediaBrowserCompat}.
+ * Connecting to a new one automatically disconnects the previous browser. Changes in the
+ * connection status are sent via {@link MediaBrowserConnector.Callback}.
  */
 
 public class MediaBrowserConnector {
 
     private static final String TAG = "MediaBrowserConnector";
 
-    /** The callback to receive the currently connected {@link MediaBrowserCompat}. */
+    /**
+     * Represents the state of the connection to the media browser service given to
+     * {@link #connectTo}.
+     */
+    public enum ConnectionStatus {
+        /**
+         * The connection request to the browser is being initiated.
+         * Sent from {@link #connectTo} just before calling {@link MediaBrowserCompat#connect}.
+         */
+        CONNECTING,
+        /**
+         * The connection to the browser has been established and it can be used.
+         * Sent from {@link MediaBrowserCompat.ConnectionCallback#onConnected} if
+         * {@link MediaBrowserCompat#isConnected} also returns true.
+         */
+        CONNECTED,
+        /**
+         * The connection to the browser was refused.
+         * Sent from {@link MediaBrowserCompat.ConnectionCallback#onConnectionFailed} or from
+         * {@link MediaBrowserCompat.ConnectionCallback#onConnected} if
+         * {@link MediaBrowserCompat#isConnected} returns false.
+         */
+        REJECTED,
+        /**
+         * The browser crashed and that calls should NOT be made to it anymore.
+         * Called from {@link MediaBrowserCompat.ConnectionCallback#onConnectionSuspended} and from
+         * {@link #connectTo} when {@link MediaBrowserCompat#connect} throws
+         * {@link IllegalStateException}.
+         */
+        SUSPENDED,
+        /**
+         * The connection to the browser is being closed.
+         * When connecting to a new browser and the old browser is connected, this is sent
+         * from {@link #connectTo} just before calling {@link MediaBrowserCompat#disconnect} on the
+         * old browser.
+         */
+        DISCONNECTING
+    }
+
+    /**
+     * Encapsulates a {@link ComponentName} with its {@link MediaBrowserCompat} and the
+     * {@link ConnectionStatus}.
+     */
+    public static class BrowsingState {
+        @NonNull public final MediaSource mMediaSource;
+        @NonNull public final MediaBrowserCompat mBrowser;
+        @NonNull public final ConnectionStatus mConnectionStatus;
+
+        @VisibleForTesting
+        public BrowsingState(@NonNull MediaSource mediaSource, @NonNull MediaBrowserCompat browser,
+                @NonNull ConnectionStatus status) {
+            mMediaSource = Preconditions.checkNotNull(mediaSource, "source can't be null");
+            mBrowser = Preconditions.checkNotNull(browser, "browser can't be null");
+            mConnectionStatus = Preconditions.checkNotNull(status, "status can't be null");
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            BrowsingState that = (BrowsingState) o;
+            return mMediaSource.equals(that.mMediaSource)
+                    && mBrowser.equals(that.mBrowser)
+                    && mConnectionStatus == that.mConnectionStatus;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mMediaSource, mBrowser, mConnectionStatus);
+        }
+    }
+
+    /** The callback to receive the current {@link MediaBrowserCompat} and its connection state. */
     public interface Callback {
-        /** When disconnecting, the given browser will be null. */
-        void onConnectedBrowserChanged(@Nullable MediaBrowserCompat browser);
+        /** Notifies the listener of connection status changes. */
+        void onBrowserConnectionChanged(@NonNull BrowsingState state);
     }
 
     private final Context mContext;
     private final Callback mCallback;
     private final int mMaxBitmapSizePx;
 
-    @Nullable private ComponentName mBrowseService;
+    @Nullable private MediaSource mMediaSource;
     @Nullable private MediaBrowserCompat mBrowser;
 
     /**
@@ -64,22 +140,34 @@
                 com.android.car.media.common.R.integer.media_items_bitmap_max_size_px);
     }
 
+    private String getSourcePackage() {
+        if (mMediaSource == null) return null;
+        return mMediaSource.getBrowseServiceComponentName().getPackageName();
+    }
+
     /** Counter so callbacks from obsolete connections can be ignored. */
     private int mBrowserConnectionCallbackCounter = 0;
 
     private class BrowserConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
 
         private final int mSequenceNumber = ++mBrowserConnectionCallbackCounter;
-        private final String mCallbackPackage = mBrowseService.getPackageName();
+        private final String mCallbackPackage = getSourcePackage();
+
+        private BrowserConnectionCallback() {
+            if (Log.isLoggable(TAG, Log.INFO)) {
+                Log.i(TAG, "New Callback: " + idHash(this));
+            }
+        }
 
         private boolean isValidCall(String method) {
             if (mSequenceNumber != mBrowserConnectionCallbackCounter) {
-                Log.e(TAG, "Ignoring callback " + method + " for " + mCallbackPackage + " seq: "
+                Log.e(TAG, "Callback: " + idHash(this) + " ignoring " + method + " for "
+                        + mCallbackPackage + " seq: "
                         + mSequenceNumber + " current: " + mBrowserConnectionCallbackCounter
-                        + " current: " + mBrowseService.getPackageName());
+                        + " package: " + getSourcePackage());
                 return false;
             } else if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, method + " " + mBrowseService.getPackageName() + idHash(mBrowser));
+                Log.d(TAG, method + " " + getSourcePackage() + " mBrowser: " + idHash(mBrowser));
             }
             return true;
         }
@@ -87,48 +175,67 @@
         @Override
         public void onConnected() {
             if (isValidCall("onConnected")) {
-                mCallback.onConnectedBrowserChanged(mBrowser);
+                if (mBrowser != null && mBrowser.isConnected()) {
+                    sendNewState(ConnectionStatus.CONNECTED);
+                } else {
+                    sendNewState(ConnectionStatus.REJECTED);
+                }
             }
         }
 
         @Override
         public void onConnectionFailed() {
             if (isValidCall("onConnectionFailed")) {
-                mCallback.onConnectedBrowserChanged(null);
+                sendNewState(ConnectionStatus.REJECTED);
             }
         }
 
         @Override
         public void onConnectionSuspended() {
             if (isValidCall("onConnectionSuspended")) {
-                mCallback.onConnectedBrowserChanged(null);
+                sendNewState(ConnectionStatus.SUSPENDED);
             }
         }
     }
 
+    private void sendNewState(ConnectionStatus cnx) {
+        if (mMediaSource == null) {
+            Log.e(TAG, "sendNewState mMediaSource is null!");
+            return;
+        }
+        if (mBrowser == null) {
+            Log.e(TAG, "sendNewState mBrowser is null!");
+            return;
+        }
+        mCallback.onBrowserConnectionChanged(new BrowsingState(mMediaSource, mBrowser, cnx));
+    }
+
     /**
-     * Creates and connects a new {@link MediaBrowserCompat} if the given {@link ComponentName}
+     * Creates and connects a new {@link MediaBrowserCompat} if the given {@link MediaSource}
      * isn't null. If needed, the previous browser is disconnected.
-     * @param browseService the ComponentName of the media browser service.
+     * @param mediaSource the media source to connect to.
      * @see MediaBrowserCompat#MediaBrowserCompat(Context, ComponentName,
      * MediaBrowserCompat.ConnectionCallback, android.os.Bundle)
      */
-    public void connectTo(@Nullable ComponentName browseService) {
+    public void connectTo(@Nullable MediaSource mediaSource) {
         if (mBrowser != null && mBrowser.isConnected()) {
             if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Disconnecting: " + mBrowseService.getPackageName() + idHash(mBrowser));
+                Log.d(TAG, "Disconnecting: " + getSourcePackage()
+                        + " mBrowser: " + idHash(mBrowser));
             }
-            mCallback.onConnectedBrowserChanged(null);
+            sendNewState(ConnectionStatus.DISCONNECTING);
             mBrowser.disconnect();
         }
 
-        mBrowseService = browseService;
-        if (mBrowseService != null) {
-            mBrowser = createMediaBrowser(mBrowseService, new BrowserConnectionCallback());
+        mMediaSource = mediaSource;
+        if (mMediaSource != null) {
+            mBrowser = createMediaBrowser(mMediaSource, new BrowserConnectionCallback());
             if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Connecting to: " + mBrowseService.getPackageName() + idHash(mBrowser));
+                Log.d(TAG, "Connecting to: " + getSourcePackage()
+                        + " mBrowser: " + idHash(mBrowser));
             }
             try {
+                sendNewState(ConnectionStatus.CONNECTING);
                 mBrowser.connect();
             } catch (IllegalStateException ex) {
                 // Is this comment still valid ?
@@ -136,6 +243,7 @@
                 // disconnected either.). In this situation, trying to connect again can throw
                 // this exception, but there is no way to know without trying.
                 Log.e(TAG, "Connection exception: " + ex);
+                sendNewState(ConnectionStatus.SUSPENDED);
             }
         } else {
             mBrowser = null;
@@ -144,10 +252,11 @@
 
     // Override for testing.
     @NonNull
-    protected MediaBrowserCompat createMediaBrowser(@NonNull ComponentName browseService,
+    protected MediaBrowserCompat createMediaBrowser(@NonNull MediaSource mediaSource,
             @NonNull MediaBrowserCompat.ConnectionCallback callback) {
         Bundle rootHints = new Bundle();
         rootHints.putInt(MediaConstants.EXTRA_MEDIA_ART_SIZE_HINT_PIXELS, mMaxBitmapSizePx);
+        ComponentName browseService = mediaSource.getBrowseServiceComponentName();
         return new MediaBrowserCompat(mContext, browseService, callback, rootHints);
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSource.java b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
index e09c248..90a5b57 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSource.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
@@ -31,6 +31,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.car.apps.common.BitmapUtils;
 import com.android.car.apps.common.IconCropper;
@@ -84,7 +85,8 @@
         }
     }
 
-    private MediaSource(@NonNull ComponentName browseService, @NonNull CharSequence displayName,
+    @VisibleForTesting
+    public MediaSource(@NonNull ComponentName browseService, @NonNull CharSequence displayName,
             @NonNull Drawable icon, @NonNull IconCropper iconCropper) {
         mBrowseService = browseService;
         mDisplayName = displayName;
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
index 4fbb202..8c93426 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
@@ -16,9 +16,6 @@
 
 package com.android.car.media.common.source;
 
-import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
-
-import static com.android.car.apps.common.util.CarAppsDebugUtils.idHash;
 import static com.android.car.arch.common.LiveDataFunctions.dataOf;
 
 import android.app.Application;
@@ -26,20 +23,17 @@
 import android.car.CarNotConnectedException;
 import android.car.media.CarMediaManager;
 import android.content.ComponentName;
-import android.media.session.MediaController;
 import android.os.Handler;
-import android.support.v4.media.MediaBrowserCompat;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.AndroidViewModel;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+
 import java.util.Objects;
 
 /**
@@ -56,10 +50,8 @@
     // Primary media source.
     private final MutableLiveData<MediaSource> mPrimaryMediaSource = dataOf(null);
 
-    // Connected browser for the primary media source.
-    private final MutableLiveData<MediaBrowserCompat> mConnectedMediaBrowser = dataOf(null);
-    // Media controller for the connected browser.
-    private final MutableLiveData<MediaControllerCompat> mMediaController = dataOf(null);
+    // Browser for the primary media source and its connection state.
+    private final MutableLiveData<BrowsingState> mBrowsingState = dataOf(null);
 
     private final Handler mHandler;
     private final CarMediaManager.MediaSourceChangedListener mMediaSourceListener;
@@ -72,8 +64,6 @@
         MediaBrowserConnector createMediaBrowserConnector(@NonNull Application application,
                 @NonNull MediaBrowserConnector.Callback connectedBrowserCallback);
 
-        MediaControllerCompat getControllerForSession(@Nullable MediaSessionCompat.Token session);
-
         Car getCarApi();
 
         CarMediaManager getCarMediaManager(Car carApi) throws CarNotConnectedException;
@@ -81,14 +71,6 @@
         MediaSource getMediaSource(ComponentName componentName);
     }
 
-    /**
-     * Returns the MediaSourceViewModel singleton tied to the application.
-     * @deprecated should use get(Application application, int mode) instead
-     */
-    public static MediaSourceViewModel get(@NonNull Application application) {
-        return get(application, MEDIA_SOURCE_MODE_PLAYBACK);
-    }
-
     /** Returns the MediaSourceViewModel singleton tied to the application. */
     public static MediaSourceViewModel get(@NonNull Application application, int mode) {
         if (sInstances[mode] == null) {
@@ -112,12 +94,6 @@
             }
 
             @Override
-            public MediaControllerCompat getControllerForSession(
-                    @Nullable MediaSessionCompat.Token token) {
-                return token == null ? null : new MediaControllerCompat(application, token);
-            }
-
-            @Override
             public Car getCarApi() {
                 return Car.createCar(application);
             }
@@ -137,7 +113,7 @@
 
     private final InputFactory mInputFactory;
     private final MediaBrowserConnector mBrowserConnector;
-    private final MediaBrowserConnector.Callback mConnectedBrowserCallback;
+    private final MediaBrowserConnector.Callback mBrowserCallback = mBrowsingState::setValue;
 
     @VisibleForTesting
     MediaSourceViewModel(@NonNull Application application, int mode,
@@ -147,23 +123,7 @@
         mInputFactory = inputFactory;
         mCar = inputFactory.getCarApi();
 
-        mConnectedBrowserCallback = browser -> {
-            mConnectedMediaBrowser.setValue(browser);
-            if (browser != null) {
-                if (!browser.isConnected()) {
-                    Log.e(TAG, "Browser is NOT connected !! "
-                            + mPrimaryMediaSource.getValue().toString() + idHash(browser));
-                    mMediaController.setValue(null);
-                } else {
-                    mMediaController.setValue(mInputFactory.getControllerForSession(
-                            browser.getSessionToken()));
-                }
-            } else {
-                mMediaController.setValue(null);
-            }
-        };
-        mBrowserConnector = inputFactory.createMediaBrowserConnector(application,
-                mConnectedBrowserCallback);
+        mBrowserConnector = inputFactory.createMediaBrowserConnector(application, mBrowserCallback);
 
         mHandler = new Handler(application.getMainLooper());
         mMediaSourceListener = componentName -> mHandler.post(
@@ -185,8 +145,8 @@
     }
 
     @VisibleForTesting
-    MediaBrowserConnector.Callback getConnectedBrowserCallback() {
-        return mConnectedBrowserCallback;
+    MediaBrowserConnector.Callback getBrowserCallback() {
+        return mBrowserCallback;
     }
 
     /**
@@ -204,21 +164,11 @@
     }
 
     /**
-     * Returns a LiveData that emits the currently connected MediaBrowser. Emits {@code null} if no
-     * MediaSource is set, if the MediaSource does not support browsing, or if the MediaBrowser is
-     * not connected.
+     * Returns a LiveData that emits a {@link BrowsingState}, or {@code null} if there is no media
+     * source.
      */
-    public LiveData<MediaBrowserCompat> getConnectedMediaBrowser() {
-        return mConnectedMediaBrowser;
-    }
-
-    /**
-     * Returns a LiveData that emits a {@link MediaController} that allows controlling this media
-     * source, or emits {@code null} if the media source doesn't support browsing or the browser is
-     * not connected.
-     */
-    public LiveData<MediaControllerCompat> getMediaController() {
-        return mMediaController;
+    public LiveData<BrowsingState> getBrowsingState() {
+        return mBrowsingState;
     }
 
     private void updateModelState(MediaSource newMediaSource) {
@@ -233,8 +183,7 @@
 
         // Recompute dependent values
         if (newMediaSource != null) {
-            ComponentName browseService = newMediaSource.getBrowseServiceComponentName();
-            mBrowserConnector.connectTo(browseService);
+            mBrowserConnector.connectTo(newMediaSource);
         }
     }
 }
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/MediaTestUtils.java b/car-media-common/tests/robotests/src/com/android/car/media/common/MediaTestUtils.java
new file mode 100644
index 0000000..84643c4
--- /dev/null
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/MediaTestUtils.java
@@ -0,0 +1,46 @@
+/*
+ * 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.android.car.media.common;
+
+import android.content.ComponentName;
+import android.graphics.Path;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.car.apps.common.IconCropper;
+import com.android.car.media.common.source.MediaSource;
+
+public class MediaTestUtils {
+
+    private MediaTestUtils() {
+    }
+
+    /** Creates a fake {@link MediaSource}. */
+    public static MediaSource newFakeMediaSource(@NonNull String pkg, @NonNull String cls) {
+        return newFakeMediaSource(new ComponentName(pkg, cls));
+    }
+
+    /** Creates a fake {@link MediaSource}. */
+    public static MediaSource newFakeMediaSource(@NonNull ComponentName browseService) {
+        String displayName = browseService.getClassName();
+        Drawable icon = new ColorDrawable();
+        IconCropper iconCropper = new IconCropper(new Path());
+        return new MediaSource(browseService, displayName, icon, iconCropper);
+    }
+}
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java
index 9d00628..2d65528 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java
@@ -17,6 +17,7 @@
 package com.android.car.media.common.playback;
 
 import static com.android.car.arch.common.LiveDataFunctions.dataOf;
+import static com.android.car.media.common.MediaTestUtils.newFakeMediaSource;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -25,7 +26,7 @@
 import static org.mockito.Mockito.when;
 import static org.robolectric.RuntimeEnvironment.application;
 
-import android.media.MediaDescription;
+import android.support.v4.media.MediaBrowserCompat;
 import android.support.v4.media.MediaDescriptionCompat;
 import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.session.MediaControllerCompat;
@@ -39,6 +40,9 @@
 import com.android.car.arch.common.testing.InstantTaskExecutorRule;
 import com.android.car.arch.common.testing.TestLifecycleOwner;
 import com.android.car.media.common.MediaItemMetadata;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionStatus;
+import com.android.car.media.common.source.MediaSource;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -53,7 +57,9 @@
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 @RunWith(RobolectricTestRunner.class)
 public class PlaybackViewModelTest {
@@ -65,13 +71,15 @@
     @Rule
     public final TestLifecycleOwner mLifecycleOwner = new TestLifecycleOwner();
 
+    private final MediaSource mMediaSource = newFakeMediaSource("test", "test");
+
+    @Mock
+    public MediaBrowserCompat mMediaBrowser;
     @Mock
     public MediaControllerCompat mMediaController;
     @Mock
     public MediaMetadataCompat mMediaMetadata;
     @Mock
-    public MediaDescription mMediaDescription;
-    @Mock
     public MediaDescriptionCompat mMediaDescriptionCompat;
     @Mock
     public PlaybackStateCompat mPlaybackState;
@@ -80,19 +88,23 @@
 
     private PlaybackViewModel mPlaybackViewModel;
 
-    private MutableLiveData<MediaControllerCompat> mMediaControllerLiveData;
+    private MutableLiveData<BrowsingState> mBrowsingStateLD;
+
+    private Map<MediaBrowserCompat, MediaControllerCompat> mBrowserToController = new HashMap<>();
 
     @Before
     public void setUp() {
+        mBrowserToController.put(mMediaBrowser, mMediaController);
         doNothing().when(mMediaController).registerCallback(mCapturedCallback.capture());
-        when(mMediaDescriptionCompat.getMediaDescription()).thenReturn(mMediaDescription);
-        when(mMediaMetadata.getDescription()).thenReturn(mMediaDescriptionCompat);
-        mMediaControllerLiveData = dataOf(mMediaController);
-        mPlaybackViewModel = new PlaybackViewModel(application, mMediaControllerLiveData);
+        mBrowsingStateLD = dataOf(
+                new BrowsingState(mMediaSource, mMediaBrowser, ConnectionStatus.CONNECTED));
+        mPlaybackViewModel = new PlaybackViewModel(application, mBrowsingStateLD,
+                browser -> mBrowserToController.get(browser));
     }
 
     @Test
     public void testGetMetadata() {
+        when(mMediaMetadata.getDescription()).thenReturn(mMediaDescriptionCompat);
         CaptureObserver<MediaItemMetadata> observer = new CaptureObserver<>();
         mPlaybackViewModel.getMetadata().observe(mLifecycleOwner, observer);
         observer.reset();
@@ -187,13 +199,13 @@
 
     @Test
     public void testChangeMediaSource_consistentController() {
-        // Ensure getters are consistent with values delivered by callback
-        when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
-        when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState);
         deliverValuesToCallbacks(mCapturedCallback, mMediaMetadata, mPlaybackState);
 
-        // Create new MediaController and associated callback captor
+        // Create new MediaBrowser, new MediaController and associated callback captor
+        MediaBrowserCompat newMediaBrowser = mock(MediaBrowserCompat.class);
         MediaControllerCompat newController = mock(MediaControllerCompat.class);
+        mBrowserToController.put(newMediaBrowser, newController);
+
         ArgumentCaptor<MediaControllerCompat.Callback> newCallbackCaptor =
                 ArgumentCaptor.forClass(MediaControllerCompat.Callback.class);
         doNothing().when(newController).registerCallback(newCallbackCaptor.capture());
@@ -201,8 +213,6 @@
         // Wire up new data for new MediaController
         MediaMetadataCompat newMetadata = mock(MediaMetadataCompat.class);
         PlaybackStateCompat newPlaybackState = mock(PlaybackStateCompat.class);
-        when(newController.getMetadata()).thenReturn(newMetadata);
-        when(newController.getPlaybackState()).thenReturn(newPlaybackState);
 
         // Ensure that all values are coming from the correct MediaController.
         mPlaybackViewModel.getMetadata().observe(mLifecycleOwner, mediaItemMetadata -> {
@@ -225,7 +235,8 @@
             }
         });
 
-        mMediaControllerLiveData.setValue(newController);
+        mBrowsingStateLD.setValue(
+                new BrowsingState(mMediaSource, newMediaBrowser, ConnectionStatus.CONNECTED));
         deliverValuesToCallbacks(newCallbackCaptor, newMetadata, newPlaybackState);
     }
 
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java
index eeccb5e..fdf5a56 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java
@@ -16,21 +16,24 @@
 
 package com.android.car.media.common.source;
 
+import static com.android.car.media.common.MediaTestUtils.newFakeMediaSource;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 import static org.robolectric.RuntimeEnvironment.application;
 
-import android.content.ComponentName;
 import android.support.v4.media.MediaBrowserCompat;
 
 import androidx.annotation.NonNull;
 
 import com.android.car.arch.common.testing.InstantTaskExecutorRule;
 import com.android.car.arch.common.testing.TestLifecycleOwner;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionStatus;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -62,10 +65,10 @@
     @Mock
     public MediaBrowserCompat mMediaBrowser2;
 
-    private final ComponentName mBrowseService1 = new ComponentName("mediaService1", "className1");
-    private final ComponentName mBrowseService2 = new ComponentName("mediaService2", "className2");
+    private final MediaSource mMediaSource1 = newFakeMediaSource("mediaService1", "className1");
+    private final MediaSource mMediaSource2 = newFakeMediaSource("mediaService2", "className2");
 
-    private final Map<ComponentName, MediaBrowserCompat> mBrowsers = new HashMap<>(2);
+    private final Map<MediaSource, MediaBrowserCompat> mBrowsers = new HashMap<>(2);
 
     private MediaBrowserConnector mBrowserConnector;
     private MediaBrowserCompat.ConnectionCallback mConnectionCallback;
@@ -73,24 +76,22 @@
     @Mock
     public MediaBrowserConnector.Callback mConnectedBrowserCallback;
     @Captor
-    private ArgumentCaptor<MediaBrowserCompat> mConnectedBrowserCaptor;
+    private ArgumentCaptor<BrowsingState> mBrowsingStateCaptor;
 
     @Before
     public void setUp() {
-        mBrowsers.put(mBrowseService1, mMediaBrowser1);
-        mBrowsers.put(mBrowseService2, mMediaBrowser2);
-        when(mMediaBrowser1.isConnected()).thenReturn(false);
-        when(mMediaBrowser2.isConnected()).thenReturn(false);
+        mBrowsers.put(mMediaSource1, mMediaBrowser1);
+        mBrowsers.put(mMediaSource2, mMediaBrowser2);
 
-        doNothing().when(mConnectedBrowserCallback).onConnectedBrowserChanged(
-                mConnectedBrowserCaptor.capture());
+        doNothing().when(mConnectedBrowserCallback).onBrowserConnectionChanged(
+                mBrowsingStateCaptor.capture());
 
         mBrowserConnector = new MediaBrowserConnector(application, mConnectedBrowserCallback) {
             @Override
-            protected MediaBrowserCompat createMediaBrowser(@NonNull ComponentName browseService,
+            protected MediaBrowserCompat createMediaBrowser(@NonNull MediaSource mediaSource,
                     @NonNull MediaBrowserCompat.ConnectionCallback callback) {
                 mConnectionCallback = callback;
-                return mBrowsers.get(browseService);
+                return mBrowsers.get(mediaSource);
             }
         };
     }
@@ -101,54 +102,77 @@
             throw new IllegalStateException("expected");
         });
 
-        mBrowserConnector.connectTo(mBrowseService1);
+        mBrowserConnector.connectTo(mMediaSource1);
         verify(mMediaBrowser1).connect();
     }
 
     @Test
     public void testConnectionCallback_onConnected() {
+        doReturn(true).when(mMediaBrowser1).isConnected();
         setConnectionAction(() -> mConnectionCallback.onConnected());
 
-        mBrowserConnector.connectTo(mBrowseService1);
+        mBrowserConnector.connectTo(mMediaSource1);
 
-        assertThat(mConnectedBrowserCaptor.getValue()).isEqualTo(mMediaBrowser1);
+        BrowsingState state = mBrowsingStateCaptor.getValue();
+        assertThat(state.mBrowser).isEqualTo(mMediaBrowser1);
+        assertThat(state.mConnectionStatus).isEqualTo(ConnectionStatus.CONNECTED);
+    }
+
+    @Test
+    public void testConnectionCallback_onConnected_inconsistentState() {
+        doReturn(false).when(mMediaBrowser1).isConnected();
+        setConnectionAction(() -> mConnectionCallback.onConnected());
+
+        mBrowserConnector.connectTo(mMediaSource1);
+
+        BrowsingState state = mBrowsingStateCaptor.getValue();
+        assertThat(state.mBrowser).isEqualTo(mMediaBrowser1);
+        assertThat(state.mConnectionStatus).isEqualTo(ConnectionStatus.REJECTED);
     }
 
     @Test
     public void testConnectionCallback_onConnectionFailed() {
         setConnectionAction(() -> mConnectionCallback.onConnectionFailed());
 
-        mBrowserConnector.connectTo(mBrowseService1);
+        mBrowserConnector.connectTo(mMediaSource1);
 
-        assertThat(mConnectedBrowserCaptor.getValue()).isNull();
+        BrowsingState state = mBrowsingStateCaptor.getValue();
+        assertThat(state.mBrowser).isEqualTo(mMediaBrowser1);
+        assertThat(state.mConnectionStatus).isEqualTo(ConnectionStatus.REJECTED);
     }
 
     @Test
     public void testConnectionCallback_onConnectionSuspended() {
+        doReturn(true).when(mMediaBrowser1).isConnected();
         setConnectionAction(() -> {
             mConnectionCallback.onConnected();
             mConnectionCallback.onConnectionSuspended();
         });
 
-        mBrowserConnector.connectTo(mBrowseService1);
+        mBrowserConnector.connectTo(mMediaSource1);
 
 
-        List<MediaBrowserCompat> browsers = mConnectedBrowserCaptor.getAllValues();
-        assertThat(browsers.get(0)).isEqualTo(mMediaBrowser1);
-        assertThat(browsers.get(1)).isNull();
+        List<BrowsingState> browsingStates = mBrowsingStateCaptor.getAllValues();
+        assertThat(browsingStates.get(0).mBrowser).isEqualTo(mMediaBrowser1);
+        assertThat(browsingStates.get(1).mBrowser).isEqualTo(mMediaBrowser1);
+        assertThat(browsingStates.get(2).mBrowser).isEqualTo(mMediaBrowser1);
+
+        assertThat(browsingStates.get(0).mConnectionStatus).isEqualTo(ConnectionStatus.CONNECTING);
+        assertThat(browsingStates.get(1).mConnectionStatus).isEqualTo(ConnectionStatus.CONNECTED);
+        assertThat(browsingStates.get(2).mConnectionStatus).isEqualTo(ConnectionStatus.SUSPENDED);
     }
 
     @Test
     public void testConnectionCallback_onConnectedIgnoredWhenLate() {
-        mBrowserConnector.connectTo(mBrowseService1);
+        mBrowserConnector.connectTo(mMediaSource1);
         MediaBrowserCompat.ConnectionCallback cb1 = mConnectionCallback;
 
-        mBrowserConnector.connectTo(mBrowseService2);
+        mBrowserConnector.connectTo(mMediaSource2);
         MediaBrowserCompat.ConnectionCallback cb2 = mConnectionCallback;
 
         cb2.onConnected();
         cb1.onConnected();
-        assertThat(mConnectedBrowserCaptor.getValue()).isEqualTo(mMediaBrowser2);
+        assertThat(mBrowsingStateCaptor.getValue().mBrowser).isEqualTo(mMediaBrowser2);
     }
 
     private void setConnectionAction(@NonNull Runnable action) {
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
index b22bba2..313f275 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
@@ -18,9 +18,11 @@
 
 import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK;
 
+import static com.android.car.apps.common.util.CarAppsDebugUtils.idHash;
+import static com.android.car.media.common.MediaTestUtils.newFakeMediaSource;
+
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.robolectric.RuntimeEnvironment.application;
 
@@ -29,15 +31,15 @@
 import android.car.media.CarMediaManager;
 import android.content.ComponentName;
 import android.support.v4.media.MediaBrowserCompat;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.car.arch.common.testing.CaptureObserver;
 import com.android.car.arch.common.testing.InstantTaskExecutorRule;
 import com.android.car.arch.common.testing.TestLifecycleOwner;
+import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState;
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionStatus;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -48,11 +50,12 @@
 import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
 
+
 @RunWith(RobolectricTestRunner.class)
 public class MediaSourceViewModelTest {
 
-    private static final String BROWSER_CONTROLLER_PACKAGE_NAME = "browser";
-    private static final String SESSION_MANAGER_CONTROLLER_PACKAGE_NAME = "mediaSessionManager";
+    private static final String TAG = "MediaSourceVMTest";
+
     @Rule
     public final MockitoRule mMockitoRule = MockitoJUnit.rule();
     @Rule
@@ -62,10 +65,6 @@
 
     @Mock
     public MediaBrowserCompat mMediaBrowser;
-    @Mock
-    public MediaControllerCompat mMediaControllerFromBrowser;
-    @Mock
-    public MediaControllerCompat mMediaControllerFromSessionManager;
 
     @Mock
     public Car mCar;
@@ -74,17 +73,12 @@
 
     private MediaSourceViewModel mViewModel;
 
-    private ComponentName mRequestedBrowseService;
+    private MediaSource mRequestedSource;
     private MediaSource mMediaSource;
 
     @Before
     public void setUp() {
-        when(mMediaControllerFromBrowser.getPackageName())
-                .thenReturn(BROWSER_CONTROLLER_PACKAGE_NAME);
-        when(mMediaControllerFromSessionManager.getPackageName())
-                .thenReturn(SESSION_MANAGER_CONTROLLER_PACKAGE_NAME);
-
-        mRequestedBrowseService = null;
+        mRequestedSource = null;
         mMediaSource = null;
     }
 
@@ -97,21 +91,17 @@
                     @NonNull MediaBrowserConnector.Callback connectedBrowserCallback) {
                 return new MediaBrowserConnector(application, connectedBrowserCallback) {
                     @Override
-                    protected MediaBrowserCompat createMediaBrowser(ComponentName browseService,
+                    protected MediaBrowserCompat createMediaBrowser(MediaSource mediaSource,
                             MediaBrowserCompat.ConnectionCallback callback) {
-                        mRequestedBrowseService = browseService;
-                        return super.createMediaBrowser(browseService, callback);
+                        mRequestedSource = mediaSource;
+                        MediaBrowserCompat bro = super.createMediaBrowser(mediaSource, callback);
+                        Log.i(TAG, "createMediaBrowser: " + idHash(bro) + " for: " + mediaSource);
+                        return bro;
                     }
                 };
             }
 
             @Override
-            public MediaControllerCompat getControllerForSession(
-                    @Nullable MediaSessionCompat.Token token) {
-                return mMediaControllerFromBrowser;
-            }
-
-            @Override
             public Car getCarApi() {
                 return mCar;
             }
@@ -140,33 +130,35 @@
 
     @Test
     public void testGetMediaController_connectedBrowser() {
-        CaptureObserver<MediaControllerCompat> observer = new CaptureObserver<>();
-        ComponentName testComponent = new ComponentName("test", "test");
-        mMediaSource = mock(MediaSource.class);
-        when(mMediaSource.getBrowseServiceComponentName()).thenReturn(testComponent);
+        CaptureObserver<BrowsingState> observer = new CaptureObserver<>();
+        mMediaSource = newFakeMediaSource("test", "test");
         when(mMediaBrowser.isConnected()).thenReturn(true);
         initializeViewModel();
 
-        mViewModel.getConnectedBrowserCallback().onConnectedBrowserChanged(mMediaBrowser);
-        mViewModel.getMediaController().observe(mLifecycleOwner, observer);
+        mViewModel.getBrowserCallback().onBrowserConnectionChanged(
+                new BrowsingState(mMediaSource, mMediaBrowser, ConnectionStatus.CONNECTED));
+        mViewModel.getBrowsingState().observe(mLifecycleOwner, observer);
 
-        assertThat(observer.getObservedValue()).isSameAs(mMediaControllerFromBrowser);
-        assertThat(mRequestedBrowseService).isEqualTo(testComponent);
+        BrowsingState browsingState = observer.getObservedValue();
+        assertThat(browsingState.mBrowser).isSameAs(mMediaBrowser);
+        assertThat(browsingState.mConnectionStatus).isEqualTo(ConnectionStatus.CONNECTED);
+        assertThat(mRequestedSource).isEqualTo(mMediaSource);
     }
 
     @Test
     public void testGetMediaController_noActiveSession_notConnected() {
-        CaptureObserver<MediaControllerCompat> observer = new CaptureObserver<>();
-        ComponentName testComponent = new ComponentName("test", "test");
-        mMediaSource = mock(MediaSource.class);
-        when(mMediaSource.getBrowseServiceComponentName()).thenReturn(testComponent);
+        CaptureObserver<BrowsingState> observer = new CaptureObserver<>();
+        mMediaSource = newFakeMediaSource("test", "test");
         when(mMediaBrowser.isConnected()).thenReturn(false);
         initializeViewModel();
 
-        mViewModel.getMediaController().observe(mLifecycleOwner, observer);
+        mViewModel.getBrowserCallback().onBrowserConnectionChanged(
+                new BrowsingState(mMediaSource, mMediaBrowser, ConnectionStatus.REJECTED));
+        mViewModel.getBrowsingState().observe(mLifecycleOwner, observer);
 
-        assertThat(observer.hasBeenNotified()).isTrue();
-        assertThat(observer.getObservedValue()).isNull();
-        assertThat(mRequestedBrowseService).isEqualTo(testComponent);
+        BrowsingState browsingState = observer.getObservedValue();
+        assertThat(browsingState.mBrowser).isSameAs(mMediaBrowser);
+        assertThat(browsingState.mConnectionStatus).isEqualTo(ConnectionStatus.REJECTED);
+        assertThat(mRequestedSource).isEqualTo(mMediaSource);
     }
 }
diff --git a/car-messenger-common/Android.bp b/car-messenger-common/Android.bp
index 6c7a82f..7b4493f 100644
--- a/car-messenger-common/Android.bp
+++ b/car-messenger-common/Android.bp
@@ -34,7 +34,6 @@
         "car-apps-common",
         "car-messenger-protos",
         "car-telephony-common",
-        "connected-device-protos",
         "libphonenumber",
     ],
 }
diff --git a/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java b/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
index ecacb51..c4f6170 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
@@ -51,8 +51,7 @@
 
     public AsyncQueryLiveData(Context context, QueryParam.Provider provider,
             ExecutorService executorService) {
-        mObservableAsyncQuery = new ObservableAsyncQuery(provider, context.getContentResolver(),
-                this::onCursorLoaded);
+        mObservableAsyncQuery = new ObservableAsyncQuery(context, provider, this::onCursorLoaded);
         mExecutorService = executorService;
     }
 
diff --git a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
index 8ed7e73..3bdf52a 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
@@ -16,6 +16,7 @@
 
 package com.android.car.telephony.common;
 
+import android.Manifest;
 import android.content.Context;
 import android.database.Cursor;
 import android.provider.ContactsContract;
@@ -119,7 +120,8 @@
                         ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
                         ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
                         ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE},
-                ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
+                ContactsContract.Contacts.DISPLAY_NAME + " ASC ",
+                Manifest.permission.READ_CONTACTS);
         mContactListAsyncQueryLiveData = new AsyncQueryLiveData<List<Contact>>(mContext,
                 QueryParam.of(contactListQueryParam), Executors.newSingleThreadExecutor()) {
             @Override
diff --git a/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java b/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
index 394b6d4..9f2f0cb 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/ObservableAsyncQuery.java
@@ -18,12 +18,15 @@
 
 import android.content.AsyncQueryHandler;
 import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
 import android.database.ContentObserver;
 import android.database.Cursor;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
 
 import com.android.car.apps.common.log.L;
 
@@ -48,6 +51,7 @@
         void onQueryFinished(@Nullable Cursor cursor);
     }
 
+    private Context mContext;
     private AsyncQueryHandler mAsyncQueryHandler;
     private QueryParam.Provider mQueryParamProvider;
     private OnQueryFinishedListener mOnQueryFinishedListener;
@@ -61,11 +65,12 @@
      * @param listener           Listener which will be called when data is available.
      */
     public ObservableAsyncQuery(
+            @NonNull Context context,
             @NonNull QueryParam.Provider queryParamProvider,
-            @NonNull ContentResolver cr,
             @NonNull OnQueryFinishedListener listener) {
-        mAsyncQueryHandler = new AsyncQueryHandlerImpl(this, cr);
-        mContentResolver = cr;
+        mContext = context;
+        mContentResolver = context.getContentResolver();
+        mAsyncQueryHandler = new AsyncQueryHandlerImpl(this, mContentResolver);
         mContentObserver = new ContentObserver(mAsyncQueryHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -88,7 +93,8 @@
 
         mToken++;
         QueryParam queryParam = mQueryParamProvider.getQueryParam();
-        if (queryParam != null) {
+        if (queryParam != null && ContextCompat.checkSelfPermission(mContext,
+                queryParam.mPermission) == PackageManager.PERMISSION_GRANTED) {
             mAsyncQueryHandler.startQuery(
                     mToken,
                     null,
diff --git a/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java b/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java
index 9628124..6ceb7c5 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/QueryParam.java
@@ -53,17 +53,21 @@
     final String[] mSelectionArgs;
     /** Used by {@link ObservableAsyncQuery#startQuery()} as query param. */
     final String mOrderBy;
+    /** Used by {@link ObservableAsyncQuery#startQuery()} to check query permission. */
+    final String mPermission;
 
     public QueryParam(
             @NonNull Uri uri,
             @Nullable String[] projection,
             @Nullable String selection,
             @Nullable String[] selectionArgs,
-            @Nullable String orderBy) {
+            @Nullable String orderBy,
+            @NonNull String permission) {
         mUri = uri;
         mProjection = projection;
         mSelection = selection;
         mSelectionArgs = selectionArgs;
         mOrderBy = orderBy;
+        mPermission = permission;
     }
 }
diff --git a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
index 40d93a7..df1713b 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
@@ -42,6 +42,7 @@
 import android.widget.ImageView;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 import androidx.core.content.ContextCompat;
 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@@ -187,17 +188,21 @@
     public static final class PhoneNumberInfo {
         private final String mPhoneNumber;
         private final String mDisplayName;
+        private final String mDisplayNameAlt;
         private final String mInitials;
         private final Uri mAvatarUri;
         private final String mTypeLabel;
+        private final String mLookupKey;
 
-        public PhoneNumberInfo(String phoneNumber, String displayName,
-                String initials, Uri avatarUri, String typeLabel) {
+        public PhoneNumberInfo(String phoneNumber, String displayName, String displayNameAlt,
+                String initials, Uri avatarUri, String typeLabel, String lookupKey) {
             mPhoneNumber = phoneNumber;
             mDisplayName = displayName;
+            mDisplayNameAlt = displayNameAlt;
             mInitials = initials;
             mAvatarUri = avatarUri;
             mTypeLabel = typeLabel;
+            mLookupKey = lookupKey;
         }
 
         public String getPhoneNumber() {
@@ -208,6 +213,10 @@
             return mDisplayName;
         }
 
+        public String getDisplayNameAlt() {
+            return mDisplayNameAlt;
+        }
+
         /**
          * Returns the initials of the contact related to the phone number. Returns null if there is
          * no related contact.
@@ -226,6 +235,12 @@
             return mTypeLabel;
         }
 
+        /** Returns the lookup key of the contact if any is found. */
+        @Nullable
+        public String getLookupKey() {
+            return mLookupKey;
+        }
+
     }
 
     /**
@@ -240,97 +255,131 @@
             return CompletableFuture.completedFuture(new PhoneNumberInfo(
                     number,
                     context.getString(R.string.unknown),
+                    context.getString(R.string.unknown),
                     null,
                     null,
-                    ""));
+                    "",
+                    null));
         }
 
         if (isVoicemailNumber(context, number)) {
             return CompletableFuture.completedFuture(new PhoneNumberInfo(
                     number,
                     context.getString(R.string.voicemail),
+                    context.getString(R.string.voicemail),
                     null,
                     makeResourceUri(context, R.drawable.ic_voicemail),
-                    ""));
+                    "",
+                    null));
+        }
+
+        return CompletableFuture.supplyAsync(() -> lookupNumberInBackground(context, number));
+    }
+
+    /** Lookup phone number info in background. */
+    @WorkerThread
+    public static PhoneNumberInfo lookupNumberInBackground(Context context, String number) {
+        if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+                != PackageManager.PERMISSION_GRANTED) {
+            String readableNumber = getReadableNumber(context, number);
+            return new PhoneNumberInfo(number, readableNumber, readableNumber, null, null, null,
+                    null);
         }
 
         if (InMemoryPhoneBook.isInitialized()) {
             Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number);
             if (contact != null) {
                 String name = contact.getDisplayName();
-                if (name == null) {
-                    name = getFormattedNumber(context, number);
+                String nameAlt = contact.getDisplayNameAlt();
+                if (TextUtils.isEmpty(name)) {
+                    name = getReadableNumber(context, number);
                 }
-
-                if (name == null) {
-                    name = context.getString(R.string.unknown);
+                if (TextUtils.isEmpty(nameAlt)) {
+                    nameAlt = name;
                 }
 
                 PhoneNumber phoneNumber = contact.getPhoneNumber(context, number);
-                CharSequence typeLabel = "";
-                if (phoneNumber != null) {
-                    typeLabel = Phone.getTypeLabel(context.getResources(),
-                            phoneNumber.getType(),
-                            phoneNumber.getLabel());
-                }
+                CharSequence typeLabel = phoneNumber == null ? "" : phoneNumber.getReadableLabel(
+                        context.getResources());
 
-                return CompletableFuture.completedFuture(new PhoneNumberInfo(
+                return new PhoneNumberInfo(
                         number,
                         name,
+                        nameAlt,
                         contact.getInitials(),
                         contact.getAvatarUri(),
-                        typeLabel.toString()));
+                        typeLabel.toString(),
+                        contact.getLookupKey());
+            }
+        } else {
+          L.d(TAG, "InMemoryPhoneBook not initialized.");
+        }
+
+        String name = null;
+        String nameAlt = null;
+        String initials = null;
+        String photoUriString = null;
+        CharSequence typeLabel = "";
+        String lookupKey = null;
+
+        ContentResolver cr = context.getContentResolver();
+        try (Cursor cursor = cr.query(
+                Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+                new String[]{
+                        PhoneLookup.DISPLAY_NAME,
+                        PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
+                        PhoneLookup.PHOTO_URI,
+                        PhoneLookup.TYPE,
+                        PhoneLookup.LABEL,
+                        PhoneLookup.LOOKUP_KEY,
+                },
+                null, null, null)) {
+
+            if (cursor != null && cursor.moveToFirst()) {
+                int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
+                int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE);
+                int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
+                int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
+                int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
+                int lookupKeyColumn = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY);
+
+                name = cursor.getString(nameColumn);
+                nameAlt = cursor.getString(altNameColumn);
+                photoUriString = cursor.getString(photoUriColumn);
+                initials = getInitials(name, nameAlt);
+
+                int type = cursor.getInt(typeColumn);
+                String label = cursor.getString(labelColumn);
+                typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
+
+                lookupKey = cursor.getString(lookupKeyColumn);
             }
         }
 
-        return CompletableFuture.supplyAsync(() -> {
-            String name = null;
-            String nameAlt = null;
-            String photoUriString = null;
-            CharSequence typeLabel = "";
-            ContentResolver cr = context.getContentResolver();
-            String initials;
-            try (Cursor cursor = cr.query(
-                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
-                    new String[]{
-                            PhoneLookup.DISPLAY_NAME,
-                            PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
-                            PhoneLookup.PHOTO_URI,
-                            PhoneLookup.TYPE,
-                            PhoneLookup.LABEL,
-                    },
-                    null, null, null)) {
+        if (TextUtils.isEmpty(name)) {
+            name = getReadableNumber(context, number);
+        }
+        if (TextUtils.isEmpty(nameAlt)) {
+            nameAlt = name;
+        }
 
-                if (cursor != null && cursor.moveToFirst()) {
-                    int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
-                    int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE);
-                    int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
-                    int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
-                    int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
+        return new PhoneNumberInfo(
+                number,
+                name,
+                nameAlt,
+                initials,
+                TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString),
+                typeLabel.toString(),
+                lookupKey);
+    }
 
-                    name = cursor.getString(nameColumn);
-                    nameAlt = cursor.getString(altNameColumn);
-                    photoUriString = cursor.getString(photoUriColumn);
-                    int type = cursor.getInt(typeColumn);
-                    String label = cursor.getString(labelColumn);
-                    typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
-                }
-            }
+    private static String getReadableNumber(Context context, String number) {
+        String readableNumber = getFormattedNumber(context, number);
 
-            initials = getInitials(name, nameAlt);
-
-            if (name == null) {
-                name = getFormattedNumber(context, number);
-            }
-
-            if (name == null) {
-                name = context.getString(R.string.unknown);
-            }
-
-            return new PhoneNumberInfo(number, name, initials,
-                    TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString),
-                    typeLabel.toString());
-        });
+        if (readableNumber == null) {
+            readableNumber = context.getString(R.string.unknown);
+        }
+        return readableNumber;
     }
 
     /**
@@ -476,6 +525,11 @@
      * Set the given phone number as the primary phone number for its associated contact.
      */
     public static void setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber) {
+        if (context.checkSelfPermission(Manifest.permission.WRITE_CONTACTS)
+                != PackageManager.PERMISSION_GRANTED) {
+            L.w(TAG, "Missing WRITE_CONTACTS permission, not setting primary number.");
+            return;
+        }
         // Update the primary values in the data record.
         ContentValues values = new ContentValues(1);
         values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
@@ -487,23 +541,6 @@
     }
 
     /**
-     * Add a contact to favorite or remove it from favorite.
-     */
-    public static int setAsFavoriteContact(Context context, Contact contact, boolean isFavorite) {
-        if (contact.isStarred() == isFavorite) {
-            return 0;
-        }
-
-        ContentValues values = new ContentValues(1);
-        values.put(ContactsContract.Contacts.STARRED, isFavorite ? 1 : 0);
-
-        String where = ContactsContract.Contacts._ID + " = ?";
-        String[] selectionArgs = new String[]{Long.toString(contact.getId())};
-        return context.getContentResolver().update(ContactsContract.Contacts.CONTENT_URI, values,
-                where, selectionArgs);
-    }
-
-    /**
      * Mark missed call log matching given phone number as read. If phone number string is not
      * valid, it will mark all new missed call log as read.
      */
diff --git a/car-ui-lib/Android.bp b/car-ui-lib/Android.bp
index 4e59773..c630252 100644
--- a/car-ui-lib/Android.bp
+++ b/car-ui-lib/Android.bp
@@ -94,6 +94,7 @@
     libs: [
         "android.test.runner",
         "android.test.base",
+        "android.test.mock.stubs",
         "android.car"
     ],
     manifest: "car-ui-lib/src/androidTest/AndroidManifest.xml",
@@ -132,11 +133,11 @@
     platform_apis: true,
     certificate: "platform",
     privileged: true,
+    libs: ["android.car-stubs"],
     static_libs: [
         "car-ui-lib",
-        "android.car.userlib",
-	"guava",
-	"gson-prebuilt-jar",
+        "guava",
+        "gson-prebuilt-jar",
     ],
     optimize: {
         enabled: false,
diff --git a/car-ui-lib/car-ui-lib/AndroidManifest-gradle.xml b/car-ui-lib/car-ui-lib/AndroidManifest-gradle.xml
index 36ffbb8..8ffb21c 100644
--- a/car-ui-lib/car-ui-lib/AndroidManifest-gradle.xml
+++ b/car-ui-lib/car-ui-lib/AndroidManifest-gradle.xml
@@ -17,10 +17,18 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.car.ui">
-    <application>
+
+   <application>
         <provider
             android:name="com.android.car.ui.core.CarUiInstaller"
             android:authorities="${applicationId}.CarUiInstaller"
             android:exported="false"/>
+
+        <provider
+            android:name="com.android.car.ui.core.SearchResultsProvider"
+            android:authorities="${applicationId}.SearchResultsProvider"
+            android:exported="true"
+            android:process="@string/car_ui_installer_process_name"
+            android:readPermission="com.android.car.ui.READ_SEARCH_RESULTS"/>
     </application>
 </manifest>
diff --git a/car-ui-lib/car-ui-lib/AndroidManifest.xml b/car-ui-lib/car-ui-lib/AndroidManifest.xml
index a85261a..2a3d658 100644
--- a/car-ui-lib/car-ui-lib/AndroidManifest.xml
+++ b/car-ui-lib/car-ui-lib/AndroidManifest.xml
@@ -33,6 +33,7 @@
             android:name="com.android.car.ui.core.SearchResultsProvider"
             android:authorities="${applicationId}.SearchResultsProvider"
             android:exported="true"
-            android:process="@string/car_ui_installer_process_name"/>
+            android:process="@string/car_ui_installer_process_name"
+            android:readPermission="com.android.car.ui.READ_SEARCH_RESULTS"/>
     </application>
 </manifest>
diff --git a/car-ui-lib/car-ui-lib/build.gradle b/car-ui-lib/car-ui-lib/build.gradle
index 201a8d6..6a18ed4 100644
--- a/car-ui-lib/car-ui-lib/build.gradle
+++ b/car-ui-lib/car-ui-lib/build.gradle
@@ -49,6 +49,10 @@
     // This is the gradle equivalent of the libs: ["android.car"] in the Android.bp
     // Which will be in the SDK 30 R3+
     useLibrary 'android.car'
+
+    useLibrary 'android.test.runner'
+    useLibrary 'android.test.base'
+    useLibrary 'android.test.mock'
 }
 
 dependencies {
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml b/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
index 79b51bb..9bb5dfa 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/AndroidManifest.xml
@@ -31,6 +31,12 @@
         <activity
             android:name="com.android.car.ui.toolbar.ToolbarTestActivity"
             android:theme="@style/Theme.CarUi.WithToolbar"/>
+        <provider
+            android:name="com.android.car.ui.core.SearchResultsProvider"
+            android:authorities="${applicationId}.SearchResultsProvider"
+            android:exported="true"
+            android:process="@string/car_ui_installer_process_name"
+            android:readPermission="com.android.car.ui.READ_SEARCH_RESULTS"/>
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.car.ui.test"
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
index b0e7c88..eff475c 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/FocusAreaTest.java
@@ -35,6 +35,7 @@
 import static com.android.car.ui.utils.RotaryConstants.NUDGE_DIRECTION;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.os.Bundle;
 import android.view.View;
@@ -66,6 +67,7 @@
     private TestFocusArea mFocusArea2;
     private TestFocusArea mFocusArea3;
     private TestFocusArea mFocusArea4;
+    private TestFocusArea mFocusArea5;
     private FocusParkingView mFpv;
     private View mView1;
     private Button mButton1;
@@ -74,6 +76,8 @@
     private View mView3;
     private View mNudgeShortcut3;
     private View mView4;
+    private View mView5;
+    private Button mButton5;
 
     @Before
     public void setUp() {
@@ -82,6 +86,7 @@
         mFocusArea2 = mActivity.findViewById(R.id.focus_area2);
         mFocusArea3 = mActivity.findViewById(R.id.focus_area3);
         mFocusArea4 = mActivity.findViewById(R.id.focus_area4);
+        mFocusArea5 = mActivity.findViewById(R.id.focus_area5);
         mFpv = mActivity.findViewById(R.id.fpv);
         mView1 = mActivity.findViewById(R.id.view1);
         mButton1 = mActivity.findViewById(R.id.button1);
@@ -90,6 +95,8 @@
         mView3 = mActivity.findViewById(R.id.view3);
         mNudgeShortcut3 = mActivity.findViewById(R.id.nudge_shortcut3);
         mView4 = mActivity.findViewById(R.id.view4);
+        mView5 = mActivity.findViewById(R.id.view5);
+        mButton5 = mActivity.findViewById(R.id.button5);
     }
 
     @Test
@@ -344,7 +351,6 @@
             RotaryCache cache =
                     new RotaryCache(CACHE_TYPE_NEVER_EXPIRE, 0, CACHE_TYPE_NEVER_EXPIRE, 0);
             mFocusArea2.setRotaryCache(cache);
-            mFocusArea2.setDefaultFocusOverridesHistory(false);
 
             mView2.requestFocus();
             assertThat(mView2.isFocused()).isTrue();
@@ -507,6 +513,44 @@
         });
     }
 
+    @Test
+    public void testWrapAround() {
+        mFocusArea1.post(() -> {
+            mView5.requestFocus();
+            assertThat(mView5.isFocused()).isTrue();
+
+            View focusSearch = mView5.focusSearch(View.FOCUS_FORWARD);
+
+            assertWithMessage("Forward wrap-around").that(focusSearch).isEqualTo(mButton5);
+
+            mButton5.requestFocus();
+            assertThat(mButton5.isFocused()).isTrue();
+
+            focusSearch = mButton5.focusSearch(View.FOCUS_BACKWARD);
+
+            assertWithMessage("Backward wrap-around").that(focusSearch).isEqualTo(mView5);
+        });
+    }
+
+    @Test
+    public void testNoWrapAround() {
+        mFocusArea1.post(() -> {
+            mButton1.requestFocus();
+            assertThat(mButton1.isFocused()).isTrue();
+
+            View focusSearch = mButton1.focusSearch(View.FOCUS_FORWARD);
+
+            assertWithMessage("Forward wrap-around").that(focusSearch).isNotEqualTo(mView1);
+
+            mView1.requestFocus();
+            assertThat(mView1.isFocused()).isTrue();
+
+            focusSearch = mView1.focusSearch(View.FOCUS_BACKWARD);
+
+            assertWithMessage("Backward wrap-around").that(focusSearch).isNotEqualTo(mButton1);
+        });
+    }
+
     private void assertBoundsOffset(
             @NonNull AccessibilityNodeInfo node, int leftPx, int topPx, int rightPx, int bottomPx) {
         Bundle extras = node.getExtras();
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/ResolverRenamingMockContext.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/ResolverRenamingMockContext.java
new file mode 100644
index 0000000..95bf762
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/ResolverRenamingMockContext.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.core;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.test.IsolatedContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+public class ResolverRenamingMockContext extends IsolatedContext {
+
+    /**
+     * The renaming prefix.
+     */
+    private static final String PREFIX = "test.";
+
+
+    /**
+     * The resolver.
+     */
+    private static final MockContentResolver RESOLVER = new MockContentResolver();
+
+    /**
+     * Constructor.
+     */
+    public ResolverRenamingMockContext(Context context) {
+        super(RESOLVER, new DelegatedMockContext(context));
+    }
+
+    public MockContentResolver getResolver() {
+        return RESOLVER;
+    }
+
+    public void addProvider(String name, ContentProvider provider) {
+        RESOLVER.addProvider(name, provider);
+    }
+
+    /**
+     * The DelegatedMockContext.
+     */
+    private static class DelegatedMockContext extends MockContext {
+
+        private Context mDelegatedContext;
+
+        DelegatedMockContext(Context context) {
+            mDelegatedContext = context;
+        }
+
+        @Override
+        public String getPackageName() {
+            return "com.android.car.ui.test";
+        }
+    }
+
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/SearchResultsProviderTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/SearchResultsProviderTest.java
new file mode 100644
index 0000000..f93e788
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/core/SearchResultsProviderTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.core;
+
+import static com.android.car.ui.core.SearchResultsProvider.CONTENT;
+import static com.android.car.ui.core.SearchResultsProvider.ITEM_ID;
+import static com.android.car.ui.core.SearchResultsProvider.PRIMARY_IMAGE_BLOB;
+import static com.android.car.ui.core.SearchResultsProvider.SEARCH_RESULTS_PROVIDER;
+import static com.android.car.ui.core.SearchResultsProvider.SECONDARY_IMAGE_BLOB;
+import static com.android.car.ui.core.SearchResultsProvider.SECONDARY_IMAGE_ID;
+import static com.android.car.ui.core.SearchResultsProvider.SUBTITLE;
+import static com.android.car.ui.core.SearchResultsProvider.TITLE;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.test.ProviderTestCase2;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.ui.test.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link SearchResultsProvider}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SearchResultsProviderTest extends ProviderTestCase2<SearchResultsProvider> {
+
+    public static final String AUTHORITY =
+            CONTENT + "com.android.car.ui.test" + SEARCH_RESULTS_PROVIDER;
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private ResolverRenamingMockContext mProviderContext;
+    private Class<SearchResultsProvider> mProviderClass;
+    private SearchResultsProvider mProvider;
+
+    public SearchResultsProviderTest() {
+        super(SearchResultsProvider.class, AUTHORITY);
+        setName("ProviderSampleTests");
+        mProviderClass = SearchResultsProvider.class;
+    }
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        mProviderContext = new ResolverRenamingMockContext(getContext());
+        mProvider = mProviderClass.newInstance();
+        assertNotNull(mProvider);
+        mProvider.attachInfo(mProviderContext, null);
+        mProviderContext.addProvider(AUTHORITY, mProvider);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Override
+    public SearchResultsProvider getProvider() {
+        return mProvider;
+    }
+
+    @Test
+    public void insert_shouldInsertTheSearchResult() {
+        ContentValues values = getRecord();
+
+        SearchResultsProvider provider = getProvider();
+
+        Uri uri = provider.insert(Uri.parse(AUTHORITY), values);
+        // length - 1
+        assertEquals(0L, ContentUris.parseId(uri));
+    }
+
+    @Test
+    public void query_shouldHaveValidData() {
+        ContentValues values = getRecord();
+
+        SearchResultsProvider provider = getProvider();
+        provider.insert(Uri.parse(AUTHORITY), values);
+        Cursor cursor = provider.query(Uri.parse(AUTHORITY), null, null, null, null);
+
+        assertNotNull(cursor);
+        assertEquals(1, cursor.getCount());
+        assertTrue(cursor.moveToFirst());
+        assertEquals("1", cursor.getString(cursor.getColumnIndex(ITEM_ID)));
+        assertEquals("1", cursor.getString(cursor.getColumnIndex(SECONDARY_IMAGE_ID)));
+        assertNotNull(cursor.getBlob(cursor.getColumnIndex(PRIMARY_IMAGE_BLOB)));
+        assertNotNull(cursor.getBlob(cursor.getColumnIndex(SECONDARY_IMAGE_BLOB)));
+        assertEquals("Title", cursor.getString(cursor.getColumnIndex(TITLE)));
+        assertEquals("SubTitle", cursor.getString(cursor.getColumnIndex(SUBTITLE)));
+    }
+
+    private byte[] bitmapToByteArray(Bitmap bitmap) {
+        Parcel parcel = Parcel.obtain();
+        bitmap.writeToParcel(parcel, 0);
+        byte[] bytes = parcel.marshall();
+        parcel.recycle();
+        return bytes;
+    }
+
+    private ContentValues getRecord() {
+        ContentValues values = new ContentValues();
+        int id = 1;
+        values.put(ITEM_ID, id);
+        values.put(SECONDARY_IMAGE_ID, id);
+        BitmapDrawable icon = (BitmapDrawable) mContext.getDrawable(R.drawable.ic_launcher);
+        values.put(PRIMARY_IMAGE_BLOB,
+                icon != null ? bitmapToByteArray(icon.getBitmap()) : null);
+        values.put(SECONDARY_IMAGE_BLOB,
+                icon != null ? bitmapToByteArray(icon.getBitmap()) : null);
+        values.put(TITLE, "Title");
+        values.put(SUBTITLE, "SubTitle");
+        return values;
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java
index 11d33f4..352a73a 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenControllerTest.java
@@ -19,59 +19,95 @@
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.assertion.ViewAssertions.matches;
 import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
+import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
+import static com.android.car.ui.core.SearchResultsProvider.ITEM_ID;
+import static com.android.car.ui.core.SearchResultsProvider.SECONDARY_IMAGE_ID;
+import static com.android.car.ui.core.SearchResultsProvider.SUBTITLE;
+import static com.android.car.ui.core.SearchResultsProvider.TITLE;
+import static com.android.car.ui.core.SearchResultsProviderTest.AUTHORITY;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TITLE_TO_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TO_CONTENT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_ERROR_DESC_TO_INPUT_AREA;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.CONTENT_AREA_SURFACE_PACKAGE;
 import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.REQUEST_RENDER_CONTENT_AREA;
 import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_ACTION;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_SEARCH_RESULTS;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenTestActivity.sCarUiImeWideScreenController;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.when;
 
 import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
 import android.inputmethodservice.ExtractEditText;
 import android.inputmethodservice.InputMethodService;
 import android.inputmethodservice.InputMethodService.Insets;
+import android.net.Uri;
 import android.os.Bundle;
 import android.view.LayoutInflater;
+import android.view.SurfaceControlViewHost;
 import android.view.View;
 import android.view.Window;
+import android.view.inputmethod.EditorInfo;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.InstrumentationRegistry;
 import androidx.test.core.app.ApplicationProvider;
+import androidx.test.espresso.matcher.BoundedMatcher;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.rule.ActivityTestRule;
 
 import com.android.car.ui.test.R;
 
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 /**
  * Unit tests for {@link CarUiImeWideScreenController}.
  */
+@RunWith(AndroidJUnit4.class)
 public class CarUiImeWideScreenControllerTest {
 
-    private Context mContext = ApplicationProvider.getApplicationContext();
+    private final Context mContext = ApplicationProvider.getApplicationContext();
 
     @Mock
-    Context mMockContext;
+    private EditorInfo mEditorInfoMock;
 
     @Mock
-    InputMethodService mInputMethodService;
+    private SurfaceControlViewHost.SurfacePackage mSurfacePackageMock;
 
     @Mock
-    Dialog mDialog;
+    private InputMethodService mInputMethodService;
 
     @Mock
-    Window mWindow;
+    private Dialog mDialog;
+
+    @Mock
+    private Window mWindow;
 
     private CarUiImeWideScreenTestActivity mActivity;
 
@@ -103,6 +139,7 @@
         onView(withId(R.id.car_ui_wideScreenErrorMessage)).check(matches(not(isDisplayed())));
         onView(withId(R.id.car_ui_wideScreenError)).check(matches(not(isDisplayed())));
         onView(withId(R.id.car_ui_contentAreaAutomotive)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_ime_surface)).check(matches(not(isDisplayed())));
 
         onView(withId(R.id.car_ui_wideScreenExtractedTextIcon)).check(matches(isDisplayed()));
         onView(withId(R.id.car_ui_wideScreenClearData)).check(matches(isDisplayed()));
@@ -160,6 +197,123 @@
         assertThat(outInsets.visibleTopInsets, is(200));
     }
 
+    @Test
+    public void onAppPrivateCommand_shouldShowTitleAndDesc() {
+        when(mInputMethodService.getWindow()).thenReturn(mDialog);
+        when(mDialog.getWindow()).thenReturn(mWindow);
+
+        sCarUiImeWideScreenController.setExtractEditText(new ExtractEditText(mContext));
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            Bundle bundle = new Bundle();
+            bundle.putString(ADD_DESC_TITLE_TO_CONTENT_AREA, "Title");
+            bundle.putString(ADD_DESC_TO_CONTENT_AREA, "Description");
+            sCarUiImeWideScreenController.onAppPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+        });
+
+        onView(withId(R.id.car_ui_wideScreenDescriptionTitle)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_wideScreenDescriptionTitle)).check(
+                matches(withText(containsString("Title"))));
+
+        onView(withId(R.id.car_ui_wideScreenDescription)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_wideScreenDescription)).check(
+                matches(withText(containsString("Description"))));
+    }
+
+    @Test
+    public void onAppPrivateCommand_shouldShowErrorMessage() {
+        when(mInputMethodService.getWindow()).thenReturn(mDialog);
+        when(mDialog.getWindow()).thenReturn(mWindow);
+
+        sCarUiImeWideScreenController.setExtractEditText(new ExtractEditText(mContext));
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            Bundle bundle = new Bundle();
+            bundle.putString(ADD_ERROR_DESC_TO_INPUT_AREA, "Error Message");
+            sCarUiImeWideScreenController.onAppPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+        });
+
+        onView(withId(R.id.car_ui_wideScreenErrorMessage)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_wideScreenErrorMessage)).check(
+                matches(withText(containsString("Error Message"))));
+        onView(withId(R.id.car_ui_wideScreenError)).check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void onAppPrivateCommand_shouldShowSearchResults() {
+        ContentResolver cr = ApplicationProvider.getApplicationContext().getContentResolver();
+        cr.insert(Uri.parse(AUTHORITY), getRecord());
+
+        when(mInputMethodService.getWindow()).thenReturn(mDialog);
+        when(mDialog.getWindow()).thenReturn(mWindow);
+
+        sCarUiImeWideScreenController.setExtractEditText(new ExtractEditText(mContext));
+        sCarUiImeWideScreenController.setEditorInfo(mEditorInfoMock);
+
+        CarUiImeWideScreenController spy = Mockito.spy(sCarUiImeWideScreenController);
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            doReturn("com.android.car.ui.test").when(spy).getPackageName(ArgumentMatchers.any());
+            Bundle bundle = new Bundle();
+            spy.onAppPrivateCommand(WIDE_SCREEN_SEARCH_RESULTS, bundle);
+        });
+
+        onView(withId(R.id.car_ui_wideScreenSearchResultList)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_wideScreenSearchResultList))
+                .check(matches(atPosition(0, hasDescendant(withText("Title")))));
+        onView(withId(R.id.car_ui_wideScreenSearchResultList))
+                .check(matches(atPosition(0, hasDescendant(withText("SubTitle")))));
+    }
+
+    @Test
+    public void onAppPrivateCommand_shouldShowSurfaceView() {
+        when(mInputMethodService.getWindow()).thenReturn(mDialog);
+        when(mDialog.getWindow()).thenReturn(mWindow);
+
+        sCarUiImeWideScreenController.setExtractEditText(new ExtractEditText(mContext));
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            Bundle bundle = new Bundle();
+            bundle.putParcelable(CONTENT_AREA_SURFACE_PACKAGE, mSurfacePackageMock);
+            sCarUiImeWideScreenController.onAppPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+        });
+
+        onView(withId(R.id.car_ui_ime_surface)).check(matches(isDisplayed()));
+    }
+
+    private static ContentValues getRecord() {
+        ContentValues values = new ContentValues();
+        int id = 1;
+        values.put(ITEM_ID, id);
+        values.put(SECONDARY_IMAGE_ID, id);
+        values.put(TITLE, "Title");
+        values.put(SUBTITLE, "SubTitle");
+        return values;
+    }
+
+    private static Matcher<View> atPosition(final int position,
+            @NonNull final Matcher<View> itemMatcher) {
+        checkNotNull(itemMatcher);
+        return new BoundedMatcher<View, RecyclerView>(RecyclerView.class) {
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("has item at position " + position + ": ");
+                itemMatcher.describeTo(description);
+            }
+
+            @Override
+            protected boolean matchesSafely(final RecyclerView view) {
+                RecyclerView.ViewHolder viewHolder = view.findViewHolderForAdapterPosition(
+                        position);
+                if (viewHolder == null) {
+                    // has no item on such position
+                    return false;
+                }
+                return itemMatcher.matches(viewHolder.itemView);
+            }
+        };
+    }
+
     private CarUiImeWideScreenController getController() {
         return new CarUiImeWideScreenController(mContext, mInputMethodService) {
             @Override
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/PreferenceTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/PreferenceTest.java
index a955cfd..b116706 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/PreferenceTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/preference/PreferenceTest.java
@@ -85,15 +85,20 @@
         mActivity.setOnPreferenceChangeListener("list", mockListener);
 
         // Check that no option is initially selected.
-        onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+                .check(matches(isNotChecked()));
 
         // Select first option.
         onView(withText(mEntries[0])).perform(click());
         // Check that first option is selected.
-        onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+                .check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+                .check(matches(isNotChecked()));
 
         // Press back to save selection.
         onView(withId(R.id.car_ui_toolbar_nav_icon)).perform(click());
@@ -103,16 +108,22 @@
         onView(withText(R.string.title_list_preference)).perform(click());
 
         // Check that first option is selected.
-        onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+                .check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+                .check(matches(isNotChecked()));
 
         // Select second option.
         onView(withText(mEntries[1])).perform(click());
         // Check that second option is selected.
-        onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+                .check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+                .check(matches(isNotChecked()));
 
         // Press back to save selection.
         onView(withId(R.id.car_ui_toolbar_nav_icon)).perform(click());
@@ -122,9 +133,12 @@
         onView(withText(R.string.title_list_preference)).perform(click());
 
         // Check that second option is selected.
-        onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+                .check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+                .check(matches(isNotChecked()));
     }
 
     @Test
@@ -141,18 +155,24 @@
         mActivity.setOnPreferenceChangeListener("multi_select_list", mockListener);
 
         // Check that no option is initially selected.
-        onView(withIndex(withId(R.id.checkbox_widget), 0)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.checkbox_widget), 1)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.checkbox_widget), 2)).check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 0))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 1))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 2))
+                .check(matches(isNotChecked()));
 
         // Select options 1 and 3.
         onView(withText(mEntries[0])).perform(click());
         onView(withText(mEntries[2])).perform(click());
 
         // Check that selections are correctly reflected.
-        onView(withIndex(withId(R.id.checkbox_widget), 0)).check(matches(isChecked()));
-        onView(withIndex(withId(R.id.checkbox_widget), 1)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.checkbox_widget), 2)).check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 0))
+                .check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 1))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 2))
+                .check(matches(isChecked()));
 
         // Press back to save selection.
         onView(withId(R.id.car_ui_toolbar_nav_icon)).perform(click());
@@ -166,9 +186,12 @@
         onView(withText(R.string.title_multi_list_preference)).perform(click());
 
         // Check that selections are correctly reflected.
-        onView(withIndex(withId(R.id.checkbox_widget), 0)).check(matches(isChecked()));
-        onView(withIndex(withId(R.id.checkbox_widget), 1)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.checkbox_widget), 2)).check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 0))
+                .check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 1))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_checkbox_widget), 2))
+                .check(matches(isChecked()));
     }
 
     @Test
@@ -277,16 +300,22 @@
         mActivity.setOnPreferenceChangeListener("dropdown", mockListener);
 
         // Check that first option is initially selected.
-        onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+                .check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+                .check(matches(isNotChecked()));
 
         // Select third option.
         onView(withText(mEntries[2])).perform(click());
         // Check that first option is selected.
-        onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+                .check(matches(isChecked()));
 
         // Press back to save selection.
         onView(withId(R.id.car_ui_toolbar_nav_icon)).perform(click());
@@ -296,9 +325,12 @@
         onView(withText(R.string.title_dropdown_preference)).perform(click());
 
         // Check that first option is selected.
-        onView(withIndex(withId(R.id.radio_button_widget), 0)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 1)).check(matches(isNotChecked()));
-        onView(withIndex(withId(R.id.radio_button_widget), 2)).check(matches(isChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 0))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 1))
+                .check(matches(isNotChecked()));
+        onView(withIndex(withId(R.id.car_ui_list_item_radio_button_widget), 2))
+                .check(matches(isChecked()));
     }
 
     @Test
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
index da446e1..3480ac9 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
@@ -71,10 +71,10 @@
         mCarUiRecyclerView.post(
                 () -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
 
-        onView(withId(R.id.title)).check(matches(isDisplayed()));
-        onView(withId(R.id.body)).check(matches(not(isDisplayed())));
-        onView(withId(R.id.icon_container)).check(matches(not(isDisplayed())));
-        onView(withId(R.id.action_container)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_body)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_list_item_icon_container)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_list_item_action_container)).check(matches(not(isDisplayed())));
     }
 
     @Test
@@ -88,10 +88,10 @@
         mCarUiRecyclerView.post(
                 () -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
 
-        onView(withId(R.id.body)).check(matches(isDisplayed()));
-        onView(withId(R.id.title)).check(matches(not(isDisplayed())));
-        onView(withId(R.id.icon_container)).check(matches(not(isDisplayed())));
-        onView(withId(R.id.action_container)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_list_item_body)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_title)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_list_item_icon_container)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_list_item_action_container)).check(matches(not(isDisplayed())));
     }
 
     @Test
@@ -107,10 +107,10 @@
         mCarUiRecyclerView.post(
                 () -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
 
-        onView(withId(R.id.title)).check(matches(isDisplayed()));
-        onView(withId(R.id.body)).check(matches(isDisplayed()));
-        onView(withId(R.id.icon_container)).check(matches(isDisplayed()));
-        onView(withId(R.id.action_container)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_body)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_icon_container)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_action_container)).check(matches(not(isDisplayed())));
     }
 
     @Test
@@ -128,21 +128,21 @@
         mCarUiRecyclerView.post(
                 () -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
 
-        onView(withId(R.id.title)).check(matches(isDisplayed()));
-        onView(withId(R.id.checkbox_widget)).check(matches(isDisplayed()));
-        onView(withId(R.id.action_divider)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_checkbox_widget)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_action_divider)).check(matches(not(isDisplayed())));
 
         // List item with checkbox should be initially unchecked.
-        onView(withId(R.id.checkbox_widget)).check(matches(isNotChecked()));
+        onView(withId(R.id.car_ui_list_item_checkbox_widget)).check(matches(isNotChecked()));
         // Clicks anywhere on the item should toggle the checkbox
-        onView(withId(R.id.title)).perform(click());
-        onView(withId(R.id.checkbox_widget)).check(matches(isChecked()));
+        onView(withId(R.id.car_ui_list_item_title)).perform(click());
+        onView(withId(R.id.car_ui_list_item_checkbox_widget)).check(matches(isChecked()));
         // Check that onCheckChangedListener was invoked.
         verify(mockOnCheckedChangeListener, times(1)).onCheckedChanged(item, true);
 
         // Uncheck checkbox with click on the action container
-        onView(withId(R.id.action_container)).perform(click());
-        onView(withId(R.id.checkbox_widget)).check(matches(isNotChecked()));
+        onView(withId(R.id.car_ui_list_item_action_container)).perform(click());
+        onView(withId(R.id.car_ui_list_item_checkbox_widget)).check(matches(isNotChecked()));
         // Check that onCheckChangedListener was invoked.
         verify(mockOnCheckedChangeListener, times(1)).onCheckedChanged(item, false);
     }
@@ -160,18 +160,18 @@
         mCarUiRecyclerView.post(
                 () -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
 
-        onView(withId(R.id.body)).check(matches(isDisplayed()));
-        onView(withId(R.id.switch_widget)).check(matches(isDisplayed()));
-        onView(withId(R.id.action_divider)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_body)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_switch_widget)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_action_divider)).check(matches(isDisplayed()));
 
         // List item with checkbox should be initially checked.
-        onView(withId(R.id.switch_widget)).check(matches(isChecked()));
+        onView(withId(R.id.car_ui_list_item_switch_widget)).check(matches(isChecked()));
         // Clicks anywhere on the item should toggle the switch
-        onView(withId(R.id.switch_widget)).perform(click());
-        onView(withId(R.id.switch_widget)).check(matches(isNotChecked()));
+        onView(withId(R.id.car_ui_list_item_switch_widget)).perform(click());
+        onView(withId(R.id.car_ui_list_item_switch_widget)).check(matches(isNotChecked()));
         // Uncheck checkbox with click on the action container
-        onView(withId(R.id.body)).perform(click());
-        onView(withId(R.id.switch_widget)).check(matches(isChecked()));
+        onView(withId(R.id.car_ui_list_item_body)).perform(click());
+        onView(withId(R.id.car_ui_list_item_switch_widget)).check(matches(isChecked()));
     }
 
     @Test
@@ -187,18 +187,18 @@
         mCarUiRecyclerView.post(
                 () -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
 
-        onView(withId(R.id.title)).check(matches(isDisplayed()));
-        onView(withId(R.id.radio_button_widget)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_radio_button_widget)).check(matches(isDisplayed()));
 
         // List item with checkbox should be initially not checked.
-        onView(withId(R.id.radio_button_widget)).check(matches(isNotChecked()));
+        onView(withId(R.id.car_ui_list_item_radio_button_widget)).check(matches(isNotChecked()));
         // Clicks anywhere on the item should toggle the radio button.
-        onView(withId(R.id.radio_button_widget)).perform(click());
-        onView(withId(R.id.radio_button_widget)).check(matches(isChecked()));
+        onView(withId(R.id.car_ui_list_item_radio_button_widget)).perform(click());
+        onView(withId(R.id.car_ui_list_item_radio_button_widget)).check(matches(isChecked()));
 
         // Repeated clicks on a selected radio button should not toggle the element once checked.
-        onView(withId(R.id.title)).perform(click());
-        onView(withId(R.id.radio_button_widget)).check(matches(isChecked()));
+        onView(withId(R.id.car_ui_list_item_title)).perform(click());
+        onView(withId(R.id.car_ui_list_item_radio_button_widget)).check(matches(isChecked()));
     }
 
     @Test
@@ -219,16 +219,16 @@
         mCarUiRecyclerView.post(
                 () -> mCarUiRecyclerView.setAdapter(new CarUiListItemAdapter(items)));
 
-        onView(withId(R.id.title)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_list_item_title)).check(matches(isDisplayed()));
 
         // Clicks anywhere on the item should toggle the listener
-        onView(withId(R.id.title)).perform(click());
+        onView(withId(R.id.car_ui_list_item_title)).perform(click());
         verify(mockOnCheckedChangeListener, times(1)).onClick(item);
 
-        onView(withId(R.id.body)).perform(click());
+        onView(withId(R.id.car_ui_list_item_body)).perform(click());
         verify(mockOnCheckedChangeListener, times(2)).onClick(item);
 
-        onView(withId(R.id.icon_container)).perform(click());
+        onView(withId(R.id.car_ui_list_item_icon_container)).perform(click());
         verify(mockOnCheckedChangeListener, times(3)).onClick(item);
     }
 
@@ -261,7 +261,7 @@
         verify(clickListener, times(1)).onClick(item);
         verify(supplementalIconClickListener, times(0)).onClick(item);
 
-        onView(withId(R.id.supplemental_icon)).perform(click());
+        onView(withId(R.id.car_ui_list_item_supplemental_icon)).perform(click());
         // Check that icon is argument for single call to click listener.
         verify(supplementalIconClickListener, times(1)).onClick(item);
 
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
index ea4a8c3..de7d34a 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
@@ -60,9 +60,11 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.FrameLayout;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
@@ -921,6 +923,108 @@
         assertThat(carUiRecyclerView.animate(), is(equalTo(recyclerViewContainer.animate())));
     }
 
+    @Test
+    public void testScrollbarVisibility_tooSmallHeight() {
+        doReturn(true).when(mTestableResources).getBoolean(R.bool.car_ui_scrollbar_enable);
+
+        // Set to anything less than 2 * (minTouchSize + margin)
+        // minTouchSize = R.dimen.car_ui_touch_target_size
+        // margin is button up top margin or button down bottom margin
+        int recyclerviewHeight = 1;
+
+        CarUiRecyclerView carUiRecyclerView = new CarUiRecyclerView(mTestableContext);
+
+        ViewGroup container = mActivity.findViewById(R.id.test_container);
+        container.post(() -> {
+            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(-1, recyclerviewHeight);
+            container.addView(carUiRecyclerView, lp);
+            carUiRecyclerView.setAdapter(new TestAdapter(100));
+        });
+
+        onView(withId(R.id.car_ui_scroll_bar)).check(matches(not(isDisplayed())));
+
+        OrientationHelper orientationHelper =
+                OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
+        int screenHeight = orientationHelper.getTotalSpace();
+
+        assertEquals(recyclerviewHeight, screenHeight);
+    }
+
+    @Test
+    public void testScrollbarVisibility_justEnoughToShowOnlyButtons() {
+        doReturn(true).when(mTestableResources).getBoolean(R.bool.car_ui_scrollbar_enable);
+
+        // R.dimen.car_ui_touch_target_size
+        float minTouchSize = mTestableResources.getDimension(R.dimen.car_ui_touch_target_size);
+        // This value is hardcoded to 15dp in the layout.
+        int margin = (int) dpToPixel(mTestableContext, 15)
+                + (int) mTestableResources.getDimension(R.dimen.car_ui_scrollbar_separator_margin);
+        // Set to 2 * (minTouchSize + margin)
+        int recyclerviewHeight = 2 * (int) (minTouchSize + margin);
+
+        CarUiRecyclerView carUiRecyclerView = new CarUiRecyclerView(mTestableContext);
+
+        ViewGroup container = mActivity.findViewById(R.id.test_container);
+        container.post(() -> {
+            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(-1, recyclerviewHeight);
+            container.addView(carUiRecyclerView, lp);
+            carUiRecyclerView.setAdapter(new TestAdapter(100));
+        });
+
+        onView(withId(R.id.car_ui_scroll_bar)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_scrollbar_thumb)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_scrollbar_track)).check(matches(not(isDisplayed())));
+        onView(withId(R.id.car_ui_scrollbar_page_down)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_scrollbar_page_up)).check(matches(isDisplayed()));
+
+        OrientationHelper orientationHelper =
+                OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
+        int screenHeight = orientationHelper.getTotalSpace();
+
+        assertEquals(recyclerviewHeight, screenHeight);
+    }
+
+    @Test
+    public void testScrollbarVisibility_enoughToShowEverything() {
+        doReturn(true).when(mTestableResources).getBoolean(R.bool.car_ui_scrollbar_enable);
+
+        // R.dimen.car_ui_touch_target_size
+        float minTouchSize = mTestableResources.getDimension(R.dimen.car_ui_touch_target_size);
+        // This value is hardcoded to 15dp in the layout.
+        int margin = (int) dpToPixel(mTestableContext, 15)
+                + (int) mTestableResources.getDimension(R.dimen.car_ui_scrollbar_separator_margin);
+        // Set to anything greater or equal to 3 * minTouchSize + 2 * margin
+        int recyclerviewHeight = 3 * (int) minTouchSize + 2 * (int) margin;
+
+        CarUiRecyclerView carUiRecyclerView = new CarUiRecyclerView(mTestableContext);
+
+        ViewGroup container = mActivity.findViewById(R.id.test_container);
+        container.post(() -> {
+            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(-1, recyclerviewHeight);
+            container.addView(carUiRecyclerView, lp);
+            carUiRecyclerView.setAdapter(new TestAdapter(100));
+        });
+
+        onView(withId(R.id.car_ui_scroll_bar)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_scrollbar_thumb)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_scrollbar_track)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_scrollbar_page_down)).check(matches(isDisplayed()));
+        onView(withId(R.id.car_ui_scrollbar_page_up)).check(matches(isDisplayed()));
+
+        OrientationHelper orientationHelper =
+                OrientationHelper.createVerticalHelper(carUiRecyclerView.getLayoutManager());
+        int screenHeight = orientationHelper.getTotalSpace();
+
+        assertEquals(recyclerviewHeight, screenHeight);
+    }
+
+    private static float dpToPixel(Context context, int dp) {
+        return TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP,
+                dp,
+                context.getResources().getDisplayMetrics());
+    }
+
     /**
      * Returns an item in the current list view whose height is taller than that of
      * the CarUiRecyclerView. If that item exists, then it is returned; otherwise an {@link
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
index 3cc9b7e..9cd5484 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
+++ b/car-ui-lib/car-ui-lib/src/androidTest/java/com/android/car/ui/utils/ViewUtilsTest.java
@@ -204,6 +204,22 @@
     }
 
     @Test
+    public void testFindImplicitDefaultFocusView_selectedItem_inFocusArea() {
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        View selectedItem = mList5.getLayoutManager().findViewByPosition(1);
+                        selectedItem.setSelected(true);
+                        View implicitDefaultFocus =
+                                ViewUtils.findImplicitDefaultFocusView(mFocusArea5);
+                        assertThat(implicitDefaultFocus).isEqualTo(selectedItem);
+                    }
+                }));
+    }
+
+    @Test
     public void testFindFirstFocusableDescendant() {
         mRoot.post(() -> {
             mFocusArea2.setFocusable(true);
@@ -286,6 +302,20 @@
     }
 
     @Test
+    public void testIsImplicitDefaultFocusView_selectedItem() {
+        mRoot.post(() -> mList5.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        mList5.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                        View selectedItem = mList5.getLayoutManager().findViewByPosition(1);
+                        selectedItem.setSelected(true);
+                        assertThat(ViewUtils.isImplicitDefaultFocusView(selectedItem)).isTrue();
+                    }
+                }));
+    }
+
+    @Test
     public void testRequestFocus() {
         mRoot.post(() -> assertRequestFocus(mView2, true));
     }
diff --git a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
index da1255d..5eba881 100644
--- a/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
+++ b/car-ui-lib/car-ui-lib/src/androidTest/res/layout/focus_area_test_activity.xml
@@ -95,4 +95,19 @@
             android:layout_width="100dp"
             android:layout_height="100dp"/>
     </com.android.car.ui.TestFocusArea>
+    <com.android.car.ui.TestFocusArea
+        android:id="@+id/focus_area5"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:wrapAround="true">
+        <View
+            android:id="@+id/view5"
+            android:focusable="true"
+            android:layout_width="100dp"
+            android:layout_height="100dp"/>
+        <Button
+            android:id="@+id/button5"
+            android:layout_width="100dp"
+            android:layout_height="100dp"/>
+    </com.android.car.ui.TestFocusArea>
 </LinearLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
index 0488d28..b20758a 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusArea.java
@@ -25,8 +25,6 @@
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_RIGHT_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.FOCUS_AREA_TOP_BOUND_OFFSET;
 import static com.android.car.ui.utils.RotaryConstants.NUDGE_DIRECTION;
-import static com.android.car.ui.utils.ViewUtils.NO_FOCUS;
-import static com.android.car.ui.utils.ViewUtils.REGULAR_FOCUS;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -38,9 +36,11 @@
 import android.os.SystemClock;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.FocusFinder;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.LinearLayout;
 
@@ -137,8 +137,8 @@
     private View mDefaultFocusView;
 
     /**
-     * Whether to focus on the {@code app:defaultFocus} view when nudging to the FocusArea, even if
-     * there was another view in the FocusArea focused before.
+     * Whether to focus on the default focus view when nudging to the FocusArea, even if there was
+     * another view in the FocusArea focused before.
      */
     private boolean mDefaultFocusOverridesHistory;
 
@@ -160,6 +160,9 @@
     /** Map of specified nudge target FocusAreas. */
     private Map<Integer, FocusArea> mSpecifiedNudgeFocusAreaMap;
 
+    /** Whether wrap-around is enabled. */
+    private boolean mWrapAround;
+
     /**
      * Cache of focus history and nudge history of the rotary controller.
      * <p>
@@ -193,6 +196,16 @@
     /** The focused view in this FocusArea, if any. */
     private View mFocusedView;
 
+    private final OnGlobalFocusChangeListener mFocusChangeListener =
+            (oldFocus, newFocus) -> {
+                boolean hasFocus = hasFocus();
+                saveFocusHistory(hasFocus);
+                maybeUpdatePreviousFocusArea(hasFocus, oldFocus);
+                maybeClearFocusAreaHistory(hasFocus, oldFocus);
+                maybeUpdateFocusAreaHighlight(hasFocus);
+                mHasFocus = hasFocus;
+            };
+
     public FocusArea(Context context) {
         super(context);
         init(context, null);
@@ -225,8 +238,6 @@
         mBackgroundHighlight = resources.getDrawable(
                 R.drawable.car_ui_focus_area_background_highlight, getContext().getTheme());
 
-        mDefaultFocusOverridesHistory = resources.getBoolean(
-                R.bool.car_ui_focus_area_default_focus_overrides_history);
         mClearFocusAreaHistoryWhenRotating = resources.getBoolean(
                 R.bool.car_ui_clear_focus_area_history_when_rotating);
 
@@ -249,29 +260,20 @@
         // should enable it since we override these methods.
         setWillNotDraw(false);
 
-        registerFocusChangeListener();
-
         initAttrs(context, attrs);
     }
 
-    private void registerFocusChangeListener() {
-        getViewTreeObserver().addOnGlobalFocusChangeListener(
-                (oldFocus, newFocus) -> {
-                    boolean hasFocus = hasFocus();
-                    saveFocusHistory(hasFocus);
-                    maybeUpdatePreviousFocusArea(hasFocus, oldFocus);
-                    maybeClearFocusAreaHistory(hasFocus, oldFocus);
-                    maybeUpdateFocusAreaHighlight(hasFocus);
-                    mHasFocus = hasFocus;
-                });
-    }
-
     private void saveFocusHistory(boolean hasFocus) {
+        // Save focus history and clear mFocusedView if focus is leaving this FocusArea.
         if (!hasFocus) {
-            mRotaryCache.saveFocusedView(mFocusedView, SystemClock.uptimeMillis());
-            mFocusedView = null;
+            if (mHasFocus) {
+                mRotaryCache.saveFocusedView(mFocusedView, SystemClock.uptimeMillis());
+                mFocusedView = null;
+            }
             return;
         }
+
+        // Update mFocusedView if a view inside this FocusArea is focused.
         View v = getFocusedChild();
         while (v != null) {
             if (v.isFocused()) {
@@ -425,6 +427,11 @@
                     a.getResourceId(R.styleable.FocusArea_nudgeUp, View.NO_ID));
             mSpecifiedNudgeIdMap.put(FOCUS_DOWN,
                     a.getResourceId(R.styleable.FocusArea_nudgeDown, View.NO_ID));
+
+            mDefaultFocusOverridesHistory = a.getBoolean(
+                    R.styleable.FocusArea_defaultFocusOverridesHistory, false);
+
+            mWrapAround = a.getBoolean(R.styleable.FocusArea_wrapAround, false);
         } finally {
             a.recycle();
         }
@@ -459,18 +466,40 @@
     }
 
     @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getViewTreeObserver().addOnGlobalFocusChangeListener(mFocusChangeListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener);
+        super.onDetachedFromWindow();
+    }
+
+    @Override
     public void onWindowFocusChanged(boolean hasWindowFocus) {
         // To ensure the focus is initialized properly in rotary mode when there is a window focus
-        // change, this FocusArea will grab the focus from the currently focused view if one of this
-        // FocusArea's descendants is a better focus candidate than the currently focused view.
+        // change, this FocusArea will grab the focus if nothing is focused or the currently
+        // focused view's FocusLevel is lower than REGULAR_FOCUS.
         if (hasWindowFocus && !isInTouchMode()) {
-            maybeAdjustFocus();
+            maybeInitFocus();
         }
         super.onWindowFocusChanged(hasWindowFocus);
     }
 
     /**
-     * Focuses on another view in this FocusArea if the view is a better focus candidate than the
+     * Focuses on another view in this FocusArea if nothing is focused or the currently focused
+     * view's FocusLevel is lower than REGULAR_FOCUS.
+     */
+    private boolean maybeInitFocus() {
+        View root = getRootView();
+        View focus = root.findFocus();
+        return ViewUtils.initFocus(root, focus);
+    }
+
+    /**
+     * Focuses on a view in this FocusArea if the view is a better focus candidate than the
      * currently focused view.
      */
     private boolean maybeAdjustFocus() {
@@ -505,22 +534,8 @@
     }
 
     private boolean focusOnDescendant() {
-        if (mDefaultFocusOverridesHistory) {
-            // Check mDefaultFocus before last focused view.
-            if (focusDefaultFocusView() || focusOnLastFocusedView()) {
-                return true;
-            }
-        } else {
-            // Check last focused view before mDefaultFocus.
-            if (focusOnLastFocusedView() || focusDefaultFocusView()) {
-                return true;
-            }
-        }
-        return focusOnFirstFocusableView();
-    }
-
-    private boolean focusDefaultFocusView() {
-        return ViewUtils.adjustFocus(this, /* currentLevel= */ REGULAR_FOCUS);
+        View lastFocusedView = mRotaryCache.getFocusedView(SystemClock.uptimeMillis());
+        return ViewUtils.adjustFocus(this, lastFocusedView, mDefaultFocusOverridesHistory);
     }
 
     /**
@@ -532,15 +547,6 @@
         return mDefaultFocusView;
     }
 
-    private boolean focusOnLastFocusedView() {
-        View lastFocusedView = mRotaryCache.getFocusedView(SystemClock.uptimeMillis());
-        return ViewUtils.requestFocus(lastFocusedView);
-    }
-
-    private boolean focusOnFirstFocusableView() {
-        return ViewUtils.adjustFocus(this, /* currentLevel= */ NO_FOCUS);
-    }
-
     private boolean nudgeToShortcutView(Bundle arguments) {
         if (mNudgeShortcutDirection == INVALID_DIRECTION) {
             // No nudge shortcut configured for this FocusArea.
@@ -720,11 +726,30 @@
         mBottomOffset = bottom;
     }
 
+    /** Sets whether wrap-around is enabled for this FocusArea. */
+    public void setWrapAround(boolean wrapAround) {
+        mWrapAround = wrapAround;
+    }
+
     /** Sets the default focus view in this FocusArea. */
     public void setDefaultFocus(@NonNull View defaultFocus) {
         mDefaultFocusView = defaultFocus;
     }
 
+    /**
+     * @inheritDoc
+     * <p>
+     * When {@link #mWrapAround} is true, the search is restricted to descendants of this
+     * {@link FocusArea}.
+     */
+    @Override
+    public View focusSearch(View focused, int direction) {
+        if (mWrapAround) {
+            return FocusFinder.getInstance().findNextFocus(/* root= */ this, focused, direction);
+        }
+        return super.focusSearch(focused, direction);
+    }
+
     @VisibleForTesting
     void enableForegroundHighlight() {
         mEnableForegroundHighlight = true;
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
index 9d5cfeb..aac5c03 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/FocusParkingView.java
@@ -22,13 +22,16 @@
 import static com.android.car.ui.utils.RotaryConstants.ACTION_RESTORE_DEFAULT_FOCUS;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.inputmethod.InputMethodManager;
 
 import androidx.annotation.Nullable;
@@ -73,6 +76,9 @@
     @Nullable
     private View mFocusedView;
 
+    /** The cache to save the previously focused view. */
+    private RotaryCache.FocusCache mFocusCache;
+
     /** The scrollable container that contains the {@link #mFocusedView}, if any. */
     @Nullable
     ViewGroup mScrollableContainer;
@@ -84,6 +90,20 @@
      */
     private boolean mShouldRestoreFocus;
 
+    /**
+     * Whether to focus on the default focus view when nudging to the explicit focus area containing
+     * this FocusParkingView, even if there was another view in the explicit focus area focused
+     * before.
+     */
+    private boolean mDefaultFocusOverridesHistory;
+
+    private final OnGlobalFocusChangeListener mFocusChangeListener =
+            (oldFocus, newFocus) -> {
+                // Keep track of the focused view so that we can recover focus when it's removed.
+                View focusedView = newFocus instanceof FocusParkingView ? null : newFocus;
+                updateFocusedView(focusedView);
+            };
+
     public FocusParkingView(Context context) {
         super(context);
         init(context, /* attrs= */ null);
@@ -127,11 +147,24 @@
         // Prevent Android from drawing the default focus highlight for this view when it's focused.
         setDefaultFocusHighlightEnabled(false);
 
-        // Keep track of the focused view so that we can recover focus when it's removed.
-        getViewTreeObserver().addOnGlobalFocusChangeListener((oldFocus, newFocus) -> {
-            mFocusedView = newFocus instanceof FocusParkingView ? null : newFocus;
-            mScrollableContainer = ViewUtils.getAncestorScrollableContainer(mFocusedView);
-        });
+        Resources resources = getResources();
+        @RotaryCache.CacheType
+        int focusHistoryCacheType = resources.getInteger(R.integer.car_ui_focus_history_cache_type);
+        int focusHistoryExpirationPeriodMs =
+                resources.getInteger(R.integer.car_ui_focus_history_expiration_period_ms);
+        mFocusCache =
+                new RotaryCache.FocusCache(focusHistoryCacheType, focusHistoryExpirationPeriodMs);
+
+        mDefaultFocusOverridesHistory = resources.getBoolean(
+                R.bool.car_ui_focus_area_default_focus_overrides_history);
+    }
+
+    private void updateFocusedView(@Nullable View focusedView) {
+        if (mFocusedView != null) {
+            mFocusCache.setFocusedView(mFocusedView, SystemClock.uptimeMillis());
+        }
+        mFocusedView = focusedView;
+        mScrollableContainer = ViewUtils.getAncestorScrollableContainer(focusedView);
     }
 
     @Override
@@ -143,6 +176,18 @@
     }
 
     @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getViewTreeObserver().addOnGlobalFocusChangeListener(mFocusChangeListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        getViewTreeObserver().removeOnGlobalFocusChangeListener(mFocusChangeListener);
+        super.onDetachedFromWindow();
+    }
+
+    @Override
     public void onWindowFocusChanged(boolean hasWindowFocus) {
         if (!hasWindowFocus) {
             // We need to clear the focus highlight(by parking the focus on the FocusParkingView)
@@ -154,8 +199,7 @@
 
             // OnGlobalFocusChangeListener won't be triggered when the window lost focus, so reset
             // the focused view here.
-            mFocusedView = null;
-            mScrollableContainer = null;
+            updateFocusedView(null);
         } else if (isFocused()) {
             // When FocusParkingView is focused and the window just gets focused, transfer the view
             // focus to a non-FocusParkingView in the window.
@@ -229,10 +273,14 @@
         if (maybeFocusOnScrollableContainer()) {
             return true;
         }
+
         // Otherwise try to find the best target view to focus.
-        if (ViewUtils.adjustFocus(getRootView(), /* currentFocus= */ null)) {
+        View cachedFocusedView = mFocusCache.getFocusedView(SystemClock.uptimeMillis());
+        if (ViewUtils.adjustFocus(
+                getRootView(), cachedFocusedView, mDefaultFocusOverridesHistory)) {
             return true;
         }
+
         // It failed to find a target view (e.g., all the views are not shown), so focus on this
         // FocusParkingView as fallback.
         return super.requestFocus(FOCUS_DOWN, /* previouslyFocusedRect= */ null);
@@ -256,7 +304,7 @@
                     // layout is not ready. So wait until its layout is ready then dispatch the
                     // event.
                     getViewTreeObserver().addOnGlobalLayoutListener(
-                            new ViewTreeObserver.OnGlobalLayoutListener() {
+                            new OnGlobalLayoutListener() {
                                 @Override
                                 public void onGlobalLayout() {
                                     // At this point the layout is complete and the dimensions of
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/RotaryCache.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/RotaryCache.java
index 18f9fb0..97de7c3 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/RotaryCache.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/RotaryCache.java
@@ -58,6 +58,7 @@
     /** A record of when a View was focused. */
     private static class FocusHistory {
         /** The focused view. */
+        @NonNull
         final View mFocusedView;
         /** The {@link SystemClock#uptimeMillis} when this history was recorded. */
         final long mTimestamp;
@@ -69,14 +70,14 @@
     }
 
     /** Cache of focused view. */
-    private static class FocusCache {
+    static class FocusCache {
         /** The cache type. */
         @CacheType
         final int mCacheType;
         /** How many milliseconds before the entry in the cache expires. */
         long mExpirationPeriodMs;
         /** The record of focused view. */
-        @NonNull
+        @Nullable
         FocusHistory mFocusHistory;
 
         FocusCache(@CacheType int cacheType, final long expirationPeriodMs) {
@@ -93,11 +94,13 @@
             return isValidHistory(elapsedRealtime) ? mFocusHistory.mFocusedView : null;
         }
 
-        void setFocusedView(@NonNull View focusedView, long elapsedRealtime) {
+        void setFocusedView(@Nullable View focusedView, long elapsedRealtime) {
             if (mCacheType == CACHE_TYPE_DISABLED) {
                 return;
             }
-            mFocusHistory = new FocusHistory(focusedView, elapsedRealtime);
+            mFocusHistory = focusedView != null
+                    ? new FocusHistory(focusedView, elapsedRealtime)
+                    : null;
         }
 
         boolean isValidHistory(long elapsedRealtime) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java
index f20c6b4..5f30b70 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/core/SearchResultsProvider.java
@@ -96,7 +96,6 @@
 
     @Override
     public int delete(Uri uri, String selection, String[] selectionArgs) {
-        getContext().getContentResolver().notifyChange(uri, null);
         mSearchResults.clear();
         return 0;
     }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java
index be8aeee..2d10605 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/imewidescreen/CarUiImeWideScreenController.java
@@ -112,6 +112,8 @@
     // Action name of action that will be used by IMS to notify the application to clear the data
     // in the EditText.
     public static final String WIDE_SCREEN_CLEAR_DATA_ACTION = "automotive_wide_screen_clear_data";
+    public static final String WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION =
+            "automotive_wide_screen_post_load_search_results";
     // Action name used by applications to notify that new search results are available.
     public static final String WIDE_SCREEN_SEARCH_RESULTS = "wide_screen_search_results";
     // Key to provide the resource id for the icon that will be displayed in the input area. If
@@ -395,52 +397,57 @@
             Log.w(TAG, "Result can't be loaded, input InputEditorInfo not available ");
             return;
         }
-        String url = CONTENT + mInputEditorInfo.packageName + SEARCH_RESULTS_PROVIDER;
+        String url = CONTENT + getPackageName(mInputEditorInfo) + SEARCH_RESULTS_PROVIDER;
         Uri contentUrl = Uri.parse(url);
         ContentResolver cr = mContext.getContentResolver();
-        Cursor c = cr.query(contentUrl, null, null, null, null);
-        mAutomotiveSearchItems = new ArrayList<>();
-        if (c != null && c.moveToFirst()) {
-            do {
-                CarUiContentListItem searchItem = new CarUiContentListItem(
-                        CarUiContentListItem.Action.ICON);
-                searchItem.setOnItemClickedListener(v -> {
-                    Bundle bundle = new Bundle();
-                    bundle.putString(SEARCH_RESULT_ITEM_ID_LIST,
-                            c.getString(c.getColumnIndex(SearchResultsProvider.ITEM_ID)));
-                    mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle);
-                });
-                searchItem.setTitle(c.getString(c.getColumnIndex(SearchResultsProvider.TITLE)));
-                searchItem.setBody(c.getString(c.getColumnIndex(SearchResultsProvider.SUBTITLE)));
-                searchItem.setPrimaryIconType(CarUiContentListItem.IconType.CONTENT);
-                byte[] primaryBlob = c.getBlob(
-                        c.getColumnIndex(SearchResultsProvider.PRIMARY_IMAGE_BLOB));
-                if (primaryBlob != null) {
-                    Bitmap primaryBitmap = Bitmap.CREATOR.createFromParcel(
-                            byteArrayToParcel(primaryBlob));
-                    searchItem.setIcon(
-                            new BitmapDrawable(mContext.getResources(), primaryBitmap));
-                }
-                byte[] secondaryBlob = c.getBlob(
-                        c.getColumnIndex(SearchResultsProvider.SECONDARY_IMAGE_BLOB));
+        try (Cursor c = cr.query(contentUrl, null, null, null, null)) {
+            mAutomotiveSearchItems = new ArrayList<>();
+            if (c != null && c.moveToFirst()) {
+                do {
+                    CarUiContentListItem searchItem = new CarUiContentListItem(
+                            CarUiContentListItem.Action.ICON);
+                    String itemId = c.getString(c.getColumnIndex(SearchResultsProvider.ITEM_ID));
+                    searchItem.setOnItemClickedListener(v -> {
+                        Bundle bundle = new Bundle();
+                        bundle.putString(SEARCH_RESULT_ITEM_ID_LIST, itemId);
+                        mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle);
+                    });
+                    searchItem.setTitle(c.getString(
+                            c.getColumnIndex(SearchResultsProvider.TITLE)));
+                    searchItem.setBody(c.getString(
+                            c.getColumnIndex(SearchResultsProvider.SUBTITLE)));
+                    searchItem.setPrimaryIconType(CarUiContentListItem.IconType.CONTENT);
+                    byte[] primaryBlob = c.getBlob(
+                            c.getColumnIndex(
+                                    SearchResultsProvider.PRIMARY_IMAGE_BLOB));
+                    if (primaryBlob != null) {
+                        Bitmap primaryBitmap = Bitmap.CREATOR.createFromParcel(
+                                byteArrayToParcel(primaryBlob));
+                        searchItem.setIcon(
+                                new BitmapDrawable(mContext.getResources(), primaryBitmap));
+                    }
+                    byte[] secondaryBlob = c.getBlob(
+                            c.getColumnIndex(
+                                    SearchResultsProvider.SECONDARY_IMAGE_BLOB));
 
-                if (secondaryBlob != null) {
-                    Bitmap secondaryBitmap = Bitmap.CREATOR.createFromParcel(
-                            byteArrayToParcel(secondaryBlob));
-                    searchItem.setSupplementalIcon(
-                            new BitmapDrawable(mContext.getResources(), secondaryBitmap), v -> {
-                                Bundle bundle = new Bundle();
-                                bundle.putString(SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST,
-                                        c.getString(c.getColumnIndex(
-                                                SearchResultsProvider.SECONDARY_IMAGE_ID)));
-                                mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION, bundle);
-                            });
-                }
-                mAutomotiveSearchItems.add(searchItem);
-            } while (c.moveToNext());
+                    if (secondaryBlob != null) {
+                        Bitmap secondaryBitmap = Bitmap.CREATOR.createFromParcel(
+                                byteArrayToParcel(secondaryBlob));
+                        searchItem.setSupplementalIcon(
+                                new BitmapDrawable(mContext.getResources(), secondaryBitmap), v -> {
+                                    Bundle bundle = new Bundle();
+                                    bundle.putString(SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST,
+                                            c.getString(c.getColumnIndex(
+                                                    SearchResultsProvider.SECONDARY_IMAGE_ID)));
+                                    mInputConnection.performPrivateCommand(WIDE_SCREEN_ACTION,
+                                            bundle);
+                                });
+                    }
+                    mAutomotiveSearchItems.add(searchItem);
+                } while (c.moveToNext());
+            }
         }
-        // delete the results.
-        cr.delete(contentUrl, null, null);
+        mInputConnection.performPrivateCommand(WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION, null);
     }
 
     private static Parcel byteArrayToParcel(byte[] bytes) {
@@ -533,7 +540,9 @@
             intiExtractAction(textForImeAction);
         }
 
-        sendSurfaceInfo();
+        if (mContentAreaSurfaceView.getVisibility() == View.GONE) {
+            sendSurfaceInfo();
+        }
     }
 
     /**
@@ -694,6 +703,16 @@
         mExtractEditText = editText;
     }
 
+    @VisibleForTesting
+    void setEditorInfo(EditorInfo editorInfo) {
+        mInputEditorInfo = editorInfo;
+    }
+
+    @VisibleForTesting
+    String getPackageName(EditorInfo editorInfo) {
+        return editorInfo.packageName;
+    }
+
     /**
      * Sets the icon in the input area. If the icon resource Id is not provided by the application
      * then application icon will be used instead.
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiDropDownPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiDropDownPreference.java
index 56134b3..7c4add1 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiDropDownPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiDropDownPreference.java
@@ -19,37 +19,41 @@
 import android.content.Context;
 import android.util.AttributeSet;
 
+import androidx.annotation.Nullable;
 import androidx.preference.DropDownPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
 
 import com.android.car.ui.R;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
 
 /**
  * This class extends the base {@link DropDownPreference} class. Adds the drawable icon to
  * the preference.
  */
-public class CarUiDropDownPreference extends DropDownPreference {
+public class CarUiDropDownPreference extends DropDownPreference
+        implements UxRestrictablePreference {
 
-    private final Context mContext;
+    private Consumer<Preference> mRestrictedClickListener;
+    private boolean mUxRestricted = false;
 
     public CarUiDropDownPreference(Context context) {
         super(context);
-        mContext = context;
     }
 
     public CarUiDropDownPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mContext = context;
     }
 
     public CarUiDropDownPreference(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        mContext = context;
     }
 
     public CarUiDropDownPreference(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        mContext = context;
     }
 
     /**
@@ -65,7 +69,7 @@
     public void onAttached() {
         super.onAttached();
 
-        boolean showChevron = mContext.getResources().getBoolean(
+        boolean showChevron = getContext().getResources().getBoolean(
                 R.bool.car_ui_preference_show_chevron);
 
         if (!showChevron) {
@@ -74,4 +78,47 @@
 
         setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
     }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+    }
+
+    @Override
+    @SuppressWarnings("RestrictTo")
+    public void performClick() {
+        if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+            if (mRestrictedClickListener != null) {
+                mRestrictedClickListener.accept(this);
+            }
+        } else {
+            super.performClick();
+        }
+    }
+
+    @Override
+    public void setUxRestricted(boolean restricted) {
+        if (restricted != mUxRestricted) {
+            mUxRestricted = restricted;
+            notifyChanged();
+        }
+    }
+
+    @Override
+    public boolean isUxRestricted() {
+        return mUxRestricted;
+    }
+
+    @Override
+    public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+        mRestrictedClickListener = listener;
+    }
+
+    @Nullable
+    @Override
+    public Consumer<Preference> getOnClickWhileRestrictedListener() {
+        return mRestrictedClickListener;
+    }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiEditTextPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiEditTextPreference.java
index 3882e03..bc21b43 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiEditTextPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiEditTextPreference.java
@@ -20,41 +20,44 @@
 import android.util.AttributeSet;
 import android.view.View;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.EditTextPreference;
+import androidx.preference.Preference;
 import androidx.preference.PreferenceViewHolder;
 
 import com.android.car.ui.R;
 import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
 
 /**
  * This class extends the base {@link EditTextPreference} class. Adds the drawable icon to
  * the preference.
  */
-public class CarUiEditTextPreference extends EditTextPreference {
+public class CarUiEditTextPreference extends EditTextPreference
+        implements UxRestrictablePreference {
 
-    private final Context mContext;
+    private Consumer<Preference> mRestrictedClickListener;
+    private boolean mUxRestricted = false;
     private boolean mShowChevron = true;
 
     public CarUiEditTextPreference(Context context, AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        mContext = context;
     }
 
     public CarUiEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mContext = context;
     }
 
     public CarUiEditTextPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mContext = context;
     }
 
     public CarUiEditTextPreference(Context context) {
         super(context);
-        mContext = context;
     }
 
     protected void setTwoActionLayout() {
@@ -73,7 +76,7 @@
     public void onAttached() {
         super.onAttached();
 
-        boolean allowChevron = mContext.getResources().getBoolean(
+        boolean allowChevron = getContext().getResources().getBoolean(
                 R.bool.car_ui_preference_show_chevron);
 
         if (!allowChevron || !mShowChevron) {
@@ -86,4 +89,47 @@
     public void setShowChevron(boolean showChevron) {
         mShowChevron = showChevron;
     }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+    }
+
+    @Override
+    @SuppressWarnings("RestrictTo")
+    public void performClick() {
+        if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+            if (mRestrictedClickListener != null) {
+                mRestrictedClickListener.accept(this);
+            }
+        } else {
+            super.performClick();
+        }
+    }
+
+    @Override
+    public void setUxRestricted(boolean restricted) {
+        if (restricted != mUxRestricted) {
+            mUxRestricted = restricted;
+            notifyChanged();
+        }
+    }
+
+    @Override
+    public boolean isUxRestricted() {
+        return mUxRestricted;
+    }
+
+    @Override
+    public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+        mRestrictedClickListener = listener;
+    }
+
+    @Nullable
+    @Override
+    public Consumer<Preference> getOnClickWhileRestrictedListener() {
+        return mRestrictedClickListener;
+    }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiListPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiListPreference.java
index 24d940c..81f11a8 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiListPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiListPreference.java
@@ -19,44 +19,47 @@
 import android.content.Context;
 import android.util.AttributeSet;
 
+import androidx.annotation.Nullable;
 import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
 
 import com.android.car.ui.R;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
 
 /**
  * This class extends the base {@link ListPreference} class. Adds the drawable icon to
  * the preference.
  */
-public class CarUiListPreference extends ListPreference {
+public class CarUiListPreference extends ListPreference implements UxRestrictablePreference {
 
-    private final Context mContext;
+    private Consumer<Preference> mRestrictedClickListener;
+    private boolean mUxRestricted = false;
 
     public CarUiListPreference(Context context, AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        mContext = context;
     }
 
     public CarUiListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mContext = context;
     }
 
     public CarUiListPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mContext = context;
     }
 
     public CarUiListPreference(Context context) {
         super(context);
-        mContext = context;
     }
 
     @Override
     public void onAttached() {
         super.onAttached();
 
-        boolean showChevron = mContext.getResources().getBoolean(
+        boolean showChevron = getContext().getResources().getBoolean(
                 R.bool.car_ui_preference_show_chevron);
 
         if (!showChevron) {
@@ -65,4 +68,47 @@
 
         setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
     }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+    }
+
+    @Override
+    @SuppressWarnings("RestrictTo")
+    public void performClick() {
+        if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+            if (mRestrictedClickListener != null) {
+                mRestrictedClickListener.accept(this);
+            }
+        } else {
+            super.performClick();
+        }
+    }
+
+    @Override
+    public void setUxRestricted(boolean restricted) {
+        if (restricted != mUxRestricted) {
+            mUxRestricted = restricted;
+            notifyChanged();
+        }
+    }
+
+    @Override
+    public boolean isUxRestricted() {
+        return mUxRestricted;
+    }
+
+    @Override
+    public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+        mRestrictedClickListener = listener;
+    }
+
+    @Nullable
+    @Override
+    public Consumer<Preference> getOnClickWhileRestrictedListener() {
+        return mRestrictedClickListener;
+    }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiMultiSelectListPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiMultiSelectListPreference.java
index bc626a0..b938325 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiMultiSelectListPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiMultiSelectListPreference.java
@@ -19,37 +19,41 @@
 import android.content.Context;
 import android.util.AttributeSet;
 
+import androidx.annotation.Nullable;
 import androidx.preference.MultiSelectListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
 
 import com.android.car.ui.R;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
 
 /**
  * This class extends the base {@link CarUiMultiSelectListPreference} class. Adds the drawable icon
  * to the preference.
  */
-public class CarUiMultiSelectListPreference extends MultiSelectListPreference {
+public class CarUiMultiSelectListPreference extends MultiSelectListPreference
+        implements UxRestrictablePreference {
 
-    private final Context mContext;
+    private Consumer<Preference> mRestrictedClickListener;
+    private boolean mUxRestricted = false;
 
     public CarUiMultiSelectListPreference(Context context) {
         super(context);
-        mContext = context;
     }
 
     public CarUiMultiSelectListPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mContext = context;
     }
 
     public CarUiMultiSelectListPreference(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        mContext = context;
     }
 
     public CarUiMultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        mContext = context;
     }
 
     /**
@@ -65,7 +69,7 @@
     public void onAttached() {
         super.onAttached();
 
-        boolean showChevron = mContext.getResources().getBoolean(
+        boolean showChevron = getContext().getResources().getBoolean(
                 R.bool.car_ui_preference_show_chevron);
 
         if (!showChevron) {
@@ -74,4 +78,47 @@
 
         setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
     }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+    }
+
+    @Override
+    @SuppressWarnings("RestrictTo")
+    public void performClick() {
+        if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+            if (mRestrictedClickListener != null) {
+                mRestrictedClickListener.accept(this);
+            }
+        } else {
+            super.performClick();
+        }
+    }
+
+    @Override
+    public void setUxRestricted(boolean restricted) {
+        if (restricted != mUxRestricted) {
+            mUxRestricted = restricted;
+            notifyChanged();
+        }
+    }
+
+    @Override
+    public boolean isUxRestricted() {
+        return mUxRestricted;
+    }
+
+    @Override
+    public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+        mRestrictedClickListener = listener;
+    }
+
+    @Nullable
+    @Override
+    public Consumer<Preference> getOnClickWhileRestrictedListener() {
+        return mRestrictedClickListener;
+    }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiPreference.java
index 8db5733..348b082 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiPreference.java
@@ -18,36 +18,31 @@
 
 import android.content.Context;
 import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Toast;
 
-import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceViewHolder;
 
 import com.android.car.ui.R;
-import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
 
 /**
  * This class extends the base {@link Preference} class. Adds the support to add a drawable icon to
  * the preference if there is one of fragment, intent or onPreferenceClickListener set.
  */
 public class CarUiPreference extends Preference implements DisabledPreferenceCallback {
-
-    private Context mContext;
     private boolean mShowChevron;
-    private String mMessageToShowWhenDisabledPreferenceClicked;
 
-    private boolean mShouldShowRippleOnDisabledPreference;
-    private Drawable mBackground;
-    private View mPreference;
+    private Consumer<Preference> mRestrictedClickListener;
+    private boolean mUxRestricted = false;
 
     public CarUiPreference(Context context, AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        init(context, attrs, defStyleAttr, defStyleRes);
+        init(attrs, defStyleAttr, defStyleRes);
     }
 
     public CarUiPreference(Context context, AttributeSet attrs, int defStyleAttr) {
@@ -62,9 +57,7 @@
         this(context, null);
     }
 
-    public void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
-        mContext = context;
-
+    private void init(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         TypedArray a = getContext().obtainStyledAttributes(
                 attrs,
                 R.styleable.CarUiPreference,
@@ -72,27 +65,23 @@
                 defStyleRes);
 
         mShowChevron = a.getBoolean(R.styleable.CarUiPreference_showChevron, true);
-        mShouldShowRippleOnDisabledPreference = a.getBoolean(
-                R.styleable.CarUiPreference_showRippleOnDisabledPreference, false);
+        mUxRestricted = a.getBoolean(R.styleable.CarUiPreference_car_ui_ux_restricted, false);
 
         a.recycle();
     }
 
-
     @Override
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
-        boolean viewEnabled = isEnabled();
-        mPreference = holder.itemView;
-        mBackground = CarUiUtils.setPreferenceViewEnabled(viewEnabled, holder.itemView, mBackground,
-                mShouldShowRippleOnDisabledPreference);
+
+        ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
     }
 
     @Override
     public void onAttached() {
         super.onAttached();
 
-        boolean allowChevron = mContext.getResources().getBoolean(
+        boolean allowChevron = getContext().getResources().getBoolean(
                 R.bool.car_ui_preference_show_chevron);
 
         if (!allowChevron || !mShowChevron) {
@@ -105,29 +94,15 @@
         }
     }
 
-    /**
-     * An exact copy of {@link androidx.preference.Preference#performClick(View)}
-     * This method was added here because super.performClick(View) is not open
-     * for app usage.
-     */
-    @SuppressWarnings("RestrictTo")
-    void performClickUnrestricted(View v) {
-        performClick();
-    }
-
-    /**
-     * This is similar to {@link Preference#performClick()} with the only difference that we do not
-     * return when view is not enabled.
-     */
     @Override
     @SuppressWarnings("RestrictTo")
     public void performClick() {
-        if (isEnabled()) {
+        if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+            if (mRestrictedClickListener != null) {
+                mRestrictedClickListener.accept(this);
+            }
+        } else {
             super.performClick();
-        } else if (mMessageToShowWhenDisabledPreferenceClicked != null
-                && !mMessageToShowWhenDisabledPreferenceClicked.isEmpty()) {
-            Toast.makeText(mContext, mMessageToShowWhenDisabledPreferenceClicked,
-                    Toast.LENGTH_LONG).show();
         }
     }
 
@@ -135,18 +110,25 @@
         mShowChevron = showChevron;
     }
 
-    /**
-     * Sets the ripple on the disabled preference.
-     */
     @Override
-    public void setShouldShowRippleOnDisabledPreference(boolean showRipple) {
-        mShouldShowRippleOnDisabledPreference = showRipple;
-        CarUiUtils.updateRippleStateOnDisabledPreference(isEnabled(),
-                mShouldShowRippleOnDisabledPreference, mBackground, mPreference);
+    public boolean isUxRestricted() {
+        return mUxRestricted;
     }
 
     @Override
-    public void setMessageToShowWhenDisabledPreferenceClicked(@NonNull String message) {
-        mMessageToShowWhenDisabledPreferenceClicked = message;
+    public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+        mRestrictedClickListener = listener;
+    }
+
+    @Nullable
+    @Override
+    public Consumer<Preference> getOnClickWhileRestrictedListener() {
+        return mRestrictedClickListener;
+    }
+
+    @Override
+    public void setUxRestricted(boolean restricted) {
+        mUxRestricted = restricted;
+        notifyChanged();
     }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiRadioButtonPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiRadioButtonPreference.java
index f02f105..8b10fc3 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiRadioButtonPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiRadioButtonPreference.java
@@ -20,14 +20,24 @@
 import android.util.AttributeSet;
 import android.widget.RadioButton;
 
+import androidx.annotation.Nullable;
+import androidx.core.content.res.TypedArrayUtils;
+import androidx.preference.Preference;
 import androidx.preference.PreferenceViewHolder;
 import androidx.preference.TwoStatePreference;
 
 import com.android.car.ui.R;
 import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
 
 /** A preference which shows a radio button at the start of the preference. */
-public class CarUiRadioButtonPreference extends TwoStatePreference {
+public class CarUiRadioButtonPreference extends TwoStatePreference
+        implements UxRestrictablePreference {
+
+    private Consumer<Preference> mRestrictedClickListener;
+    private boolean mUxRestricted = false;
 
     public CarUiRadioButtonPreference(Context context, AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
@@ -36,18 +46,18 @@
     }
 
     public CarUiRadioButtonPreference(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        init();
+        this(context, attrs, defStyleAttr, /* defStyleRes= */ 0);
     }
 
     public CarUiRadioButtonPreference(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        init();
+        // Reusing preferenceStyle since there is no separate style for TwoStatePreference or
+        // CarUiRadioButtonPreference.
+        this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle,
+                android.R.attr.preferenceStyle));
     }
 
     public CarUiRadioButtonPreference(Context context) {
-        super(context);
-        init();
+        this(context, /* attrs= */ null);
     }
 
     private void init() {
@@ -56,11 +66,49 @@
     }
 
     @Override
+    @SuppressWarnings("RestrictTo")
+    public void performClick() {
+        if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+            if (mRestrictedClickListener != null) {
+                mRestrictedClickListener.accept(this);
+            }
+        } else {
+            super.performClick();
+        }
+    }
+
+    @Override
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
 
         RadioButton radioButton = (RadioButton) CarUiUtils.findViewByRefId(holder.itemView,
                 R.id.radio_button);
         radioButton.setChecked(isChecked());
+
+        ViewUtils.makeAllViewsUxRestricted(holder.itemView, mUxRestricted);
+    }
+
+    @Override
+    public void setUxRestricted(boolean restricted) {
+        if (restricted != mUxRestricted) {
+            mUxRestricted = restricted;
+            notifyChanged();
+        }
+    }
+
+    @Override
+    public boolean isUxRestricted() {
+        return mUxRestricted;
+    }
+
+    @Override
+    public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+        mRestrictedClickListener = listener;
+    }
+
+    @Nullable
+    @Override
+    public Consumer<Preference> getOnClickWhileRestrictedListener() {
+        return mRestrictedClickListener;
     }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSeekBarDialogPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSeekBarDialogPreference.java
index 29e9e33..04f73ff 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSeekBarDialogPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSeekBarDialogPreference.java
@@ -25,13 +25,18 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.preference.DialogPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
 
 import com.android.car.ui.R;
 import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
 
 /** A class implements some basic methods of a seekbar dialog preference. */
 public class CarUiSeekBarDialogPreference extends DialogPreference
-        implements DialogFragmentCallbacks {
+        implements DialogFragmentCallbacks, UxRestrictablePreference {
 
     private int mSeekBarProgress;
     private SeekBar mSeekBar;
@@ -51,6 +56,9 @@
     private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener;
     private int mMaxProgress = 100;
 
+    private Consumer<Preference> mRestrictedClickListener;
+    private boolean mUxRestricted = false;
+
     public CarUiSeekBarDialogPreference(Context context, AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
@@ -265,4 +273,47 @@
         }
         mMaxProgress = maxProgress;
     }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
+    }
+
+    @Override
+    @SuppressWarnings("RestrictTo")
+    public void performClick() {
+        if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+            if (mRestrictedClickListener != null) {
+                mRestrictedClickListener.accept(this);
+            }
+        } else {
+            super.performClick();
+        }
+    }
+
+    @Override
+    public void setUxRestricted(boolean restricted) {
+        if (restricted != mUxRestricted) {
+            mUxRestricted = restricted;
+            notifyChanged();
+        }
+    }
+
+    @Override
+    public boolean isUxRestricted() {
+        return mUxRestricted;
+    }
+
+    @Override
+    public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+        mRestrictedClickListener = listener;
+    }
+
+    @Nullable
+    @Override
+    public Consumer<Preference> getOnClickWhileRestrictedListener() {
+        return mRestrictedClickListener;
+    }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSwitchPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSwitchPreference.java
index e2b141d..bfd7a1a 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSwitchPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiSwitchPreference.java
@@ -18,97 +18,94 @@
 
 import android.content.Context;
 import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Toast;
 
-import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
 import androidx.preference.PreferenceViewHolder;
 import androidx.preference.SwitchPreference;
 
 import com.android.car.ui.R;
-import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.ViewUtils;
+
+import java.util.function.Consumer;
 
 /**
- * This class extends the base {@link SwitchPreference} class. Adds the functionality to show
- * message when preference is disabled.
+ * This class is the same as the base {@link SwitchPreference} class, except it implements
+ * {@link UxRestrictablePreference}
  */
 public class CarUiSwitchPreference extends SwitchPreference implements DisabledPreferenceCallback {
 
-    private String mMessageToShowWhenDisabledPreferenceClicked;
-
-    private boolean mShouldShowRippleOnDisabledPreference;
-    private Drawable mBackground;
-    private View mPreference;
-    private Context mContext;
+    private Consumer<Preference> mRestrictedClickListener;
+    private boolean mUxRestricted = false;
 
     public CarUiSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        init(context, attrs);
+        init(attrs);
     }
 
     public CarUiSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        init(context, attrs);
+        init(attrs);
     }
 
     public CarUiSwitchPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-        init(context, attrs);
+        init(attrs);
     }
 
     public CarUiSwitchPreference(Context context) {
         super(context);
-        init(context, null);
+        init(null);
     }
 
-    private void init(Context context, AttributeSet attrs) {
-        mContext = context;
-        TypedArray preferenceAttributes = getContext().obtainStyledAttributes(attrs,
-                R.styleable.CarUiPreference);
-        mShouldShowRippleOnDisabledPreference = preferenceAttributes.getBoolean(
-                R.styleable.CarUiPreference_showRippleOnDisabledPreference, false);
-        preferenceAttributes.recycle();
+    private void init(AttributeSet attrs) {
+        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CarUiPreference);
+        mUxRestricted = a.getBoolean(R.styleable.CarUiPreference_car_ui_ux_restricted, false);
+        a.recycle();
     }
 
     @Override
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
-        mPreference = holder.itemView;
-        mBackground = CarUiUtils.setPreferenceViewEnabled(isEnabled(), holder.itemView, mBackground,
-                mShouldShowRippleOnDisabledPreference);
+
+        ViewUtils.makeAllViewsUxRestricted(holder.itemView, isUxRestricted());
     }
 
-    /**
-     * This is similar to {@link Preference#performClick()} with the only difference that we do not
-     * return when view is not enabled.
-     */
     @Override
     @SuppressWarnings("RestrictTo")
     public void performClick() {
-        if (isEnabled()) {
+        if ((isEnabled() || isSelectable()) && isUxRestricted()) {
+            if (mRestrictedClickListener != null) {
+                mRestrictedClickListener.accept(this);
+            }
+        } else {
             super.performClick();
-        } else if (mMessageToShowWhenDisabledPreferenceClicked != null
-                && !mMessageToShowWhenDisabledPreferenceClicked.isEmpty()) {
-            Toast.makeText(mContext, mMessageToShowWhenDisabledPreferenceClicked,
-                    Toast.LENGTH_LONG).show();
         }
     }
 
-    /**
-     * Sets the ripple on the disabled preference.
-     */
     @Override
-    public void setShouldShowRippleOnDisabledPreference(boolean showRipple) {
-        mShouldShowRippleOnDisabledPreference = showRipple;
-        CarUiUtils.updateRippleStateOnDisabledPreference(isEnabled(),
-                mShouldShowRippleOnDisabledPreference, mBackground, mPreference);
+    public void setUxRestricted(boolean restricted) {
+        if (mUxRestricted != restricted) {
+            mUxRestricted = restricted;
+            notifyChanged();
+        }
     }
 
     @Override
-    public void setMessageToShowWhenDisabledPreferenceClicked(@NonNull String message) {
-        mMessageToShowWhenDisabledPreferenceClicked = message;
+    public boolean isUxRestricted() {
+        return mUxRestricted;
+    }
+
+    @Override
+    public void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener) {
+        mRestrictedClickListener = listener;
+    }
+
+    @Nullable
+    @Override
+    public Consumer<Preference> getOnClickWhileRestrictedListener() {
+        return mRestrictedClickListener;
     }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionBasePreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionBasePreference.java
new file mode 100644
index 0000000..5151642
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionBasePreference.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.preference;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleableRes;
+
+import com.android.car.ui.R;
+
+/**
+ * A base class for several types of preferences, that all have a main click action along
+ * with a secondary action.
+ */
+public abstract class CarUiTwoActionBasePreference extends CarUiPreference {
+
+    protected boolean mSecondaryActionEnabled = true;
+    protected boolean mSecondaryActionVisible = true;
+
+    public CarUiTwoActionBasePreference(Context context,
+            AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(attrs);
+    }
+
+    public CarUiTwoActionBasePreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(attrs);
+    }
+
+    public CarUiTwoActionBasePreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(attrs);
+    }
+
+    public CarUiTwoActionBasePreference(Context context) {
+        super(context);
+        init(null);
+    }
+
+    @CallSuper
+    protected void init(@Nullable AttributeSet attrs) {
+        setShowChevron(false);
+
+        TypedArray a = getContext()
+                .obtainStyledAttributes(attrs, R.styleable.CarUiTwoActionBasePreference);
+        try {
+            disallowResourceIds(a,
+                    R.styleable.CarUiTwoActionBasePreference_layout,
+                    R.styleable.CarUiTwoActionBasePreference_android_layout,
+                    R.styleable.CarUiTwoActionBasePreference_widgetLayout,
+                    R.styleable.CarUiTwoActionBasePreference_android_widgetLayout);
+        } finally {
+            a.recycle();
+        }
+
+        a = getContext().obtainStyledAttributes(attrs,
+                R.styleable.CarUiTwoActionPreference);
+
+        try {
+            mSecondaryActionVisible = a.getBoolean(
+                    R.styleable.CarUiTwoActionPreference_actionShown, true);
+            mSecondaryActionEnabled = a.getBoolean(
+                    R.styleable.CarUiTwoActionPreference_actionEnabled, true);
+        } finally {
+            a.recycle();
+        }
+    }
+
+    /**
+     * Returns whether or not the secondary action is enabled.
+     */
+    public boolean isSecondaryActionEnabled() {
+        return mSecondaryActionEnabled && isEnabled();
+    }
+
+    /**
+     * Sets whether or not the secondary action is enabled. This is secondary to the overall
+     * {@link #setEnabled(boolean)} of the preference
+     */
+    public void setSecondaryActionEnabled(boolean enabled) {
+        mSecondaryActionEnabled = enabled;
+        notifyChanged();
+    }
+
+    /**
+     * Returns whether or not the secondary action is visible.
+     */
+    public boolean isSecondaryActionVisible() {
+        return mSecondaryActionVisible;
+    }
+
+    /**
+     * Sets whether or not the secondary action is visible.
+     */
+    public void setSecondaryActionVisible(boolean visible) {
+        mSecondaryActionVisible = visible;
+        notifyChanged();
+    }
+
+    /**
+     * Like {@link #onClick()}, but for the secondary action.
+     */
+    public void performSecondaryActionClick() {
+        if (mSecondaryActionEnabled && mSecondaryActionVisible) {
+            performSecondaryActionClickInternal();
+        }
+    }
+
+    protected abstract void performSecondaryActionClickInternal();
+
+    protected void setLayoutResourceInternal(@LayoutRes int layoutResId) {
+        super.setLayoutResource(layoutResId);
+    }
+
+    @Override
+    public void setLayoutResource(@LayoutRes int layoutResId) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setWidgetLayoutResource(@LayoutRes int widgetLayoutResId) {
+        throw new UnsupportedOperationException();
+    }
+
+    private static void disallowResourceIds(@NonNull TypedArray a, @StyleableRes int ...indices) {
+        for (int index : indices) {
+            if (a.hasValue(index)) {
+                throw new AssertionError("Setting this attribute is not allowed: "
+                        + a.getResources().getResourceName(index));
+            }
+        }
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionIconPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionIconPreference.java
new file mode 100644
index 0000000..57c65df
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionIconPreference.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.preference;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.car.ui.R;
+
+import java.util.function.Consumer;
+
+/**
+ * A preference that has an icon button that can be pressed independently of pressing the main
+ * body of the preference.
+ */
+public class CarUiTwoActionIconPreference extends CarUiTwoActionBasePreference {
+    @Nullable
+    protected Runnable mSecondaryActionOnClickListener;
+    @Nullable
+    private Drawable mSecondaryActionIcon;
+
+    public CarUiTwoActionIconPreference(Context context,
+            AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public CarUiTwoActionIconPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public CarUiTwoActionIconPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public CarUiTwoActionIconPreference(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void init(@Nullable AttributeSet attrs) {
+        super.init(attrs);
+
+        TypedArray a = getContext().obtainStyledAttributes(attrs,
+                R.styleable.CarUiTwoActionIconPreference);
+        try {
+            mSecondaryActionIcon = a.getDrawable(
+                    R.styleable.CarUiTwoActionIconPreference_secondaryActionIcon);
+        } finally {
+            a.recycle();
+        }
+
+        setLayoutResourceInternal(R.layout.car_ui_preference_two_action_icon);
+    }
+
+    @Override
+    protected void performSecondaryActionClickInternal() {
+        if (isSecondaryActionEnabled()) {
+            if (isUxRestricted()) {
+                Consumer<Preference> restrictedListener = getOnClickWhileRestrictedListener();
+                if (restrictedListener != null) {
+                    restrictedListener.accept(this);
+                }
+            } else if (mSecondaryActionOnClickListener != null) {
+                mSecondaryActionOnClickListener.run();
+            }
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        View firstActionContainer = requireViewByRefId(holder.itemView,
+                R.id.car_ui_first_action_container);
+        View secondActionContainer = requireViewByRefId(holder.itemView,
+                R.id.car_ui_second_action_container);
+        ViewGroup secondaryButton = requireViewByRefId(holder.itemView,
+                R.id.car_ui_secondary_action);
+        ImageView iconView = requireViewByRefId(holder.itemView,
+                R.id.car_ui_secondary_action_concrete);
+
+        holder.itemView.setFocusable(false);
+        holder.itemView.setClickable(false);
+
+        firstActionContainer.setOnClickListener(this::performClick);
+        firstActionContainer.setEnabled(isEnabled());
+        firstActionContainer.setFocusable(isEnabled());
+
+        secondActionContainer.setVisibility(mSecondaryActionVisible ? View.VISIBLE : View.GONE);
+        iconView.setImageDrawable(mSecondaryActionIcon);
+        iconView.setEnabled(isSecondaryActionEnabled());
+        secondaryButton.setEnabled(isSecondaryActionEnabled());
+        secondaryButton.setFocusable(isSecondaryActionEnabled());
+        secondaryButton.setOnClickListener(v -> performSecondaryActionClickInternal());
+    }
+
+    /**
+     * Sets the icon of the secondary action.
+     *
+     * The icon will be tinted to the primary text color, and resized to fit the space.
+     *
+     * @param drawable A {@link Drawable} to set as the icon.
+     */
+    public void setSecondaryActionIcon(@Nullable Drawable drawable) {
+        mSecondaryActionIcon = drawable;
+        notifyChanged();
+    }
+
+    /**
+     * Sets the icon of the secondary action.
+     *
+     * The icon will be tinted to the primary text color, and resized to fit the space.
+     *
+     * @param resid A drawable resource id to set as the icon.
+     */
+    public void setSecondaryActionIcon(@DrawableRes int resid) {
+        setSecondaryActionIcon(ContextCompat.getDrawable(getContext(), resid));
+    }
+
+    /**
+     * Sets the on-click listener of the secondary action button.
+     */
+    public void setOnSecondaryActionClickListener(@Nullable Runnable onClickListener) {
+        mSecondaryActionOnClickListener = onClickListener;
+        notifyChanged();
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionPreference.java
index 3b9693a..96fb8d5 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionPreference.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionPreference.java
@@ -30,6 +30,10 @@
 /**
  * A preference which can perform two actions. The secondary action is shown by default.
  * {@link #showAction(boolean)} may be used to manually set the visibility of the action.
+ *
+ * @deprecated This preference relies on the use of custom views. Use
+ *             {@link CarUiTwoActionTextPreference}, {@link CarUiTwoActionSwitchPreference}, or
+ *             {@link CarUiTwoActionIconPreference} instead.
  */
 public class CarUiTwoActionPreference extends CarUiPreference {
 
@@ -95,7 +99,7 @@
         View widgetFrame = CarUiUtils.findViewByRefId(holder.itemView, android.R.id.widget_frame);
         holder.itemView.setFocusable(!mIsActionShown);
         containerWithoutWidget.setOnClickListener(
-                mIsActionShown ? this::performClickUnrestricted : null);
+                mIsActionShown ? this::performClick : null);
         containerWithoutWidget.setClickable(mIsActionShown);
         containerWithoutWidget.setFocusable(mIsActionShown);
         actionContainer.setVisibility(mIsActionShown ? View.VISIBLE : View.GONE);
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionSwitchPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionSwitchPreference.java
new file mode 100644
index 0000000..22497cc
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionSwitchPreference.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.preference;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Switch;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.car.ui.R;
+
+import java.util.function.Consumer;
+
+/**
+ * A preference that has a switch that can be toggled independently of pressing the main
+ * body of the preference.
+ */
+public class CarUiTwoActionSwitchPreference extends CarUiTwoActionBasePreference {
+    @Nullable
+    protected Consumer<Boolean> mSecondaryActionOnClickListener;
+    private boolean mSecondaryActionChecked;
+
+    public CarUiTwoActionSwitchPreference(Context context,
+            AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public CarUiTwoActionSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public CarUiTwoActionSwitchPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public CarUiTwoActionSwitchPreference(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void init(@Nullable AttributeSet attrs) {
+        super.init(attrs);
+
+        setLayoutResourceInternal(R.layout.car_ui_preference_two_action_switch);
+    }
+
+    @Override
+    protected void performSecondaryActionClickInternal() {
+        if (isSecondaryActionEnabled()) {
+            if (isUxRestricted()) {
+                Consumer<Preference> restrictedListener = getOnClickWhileRestrictedListener();
+                if (restrictedListener != null) {
+                    restrictedListener.accept(this);
+                }
+            } else {
+                mSecondaryActionChecked = !mSecondaryActionChecked;
+                notifyChanged();
+                if (mSecondaryActionOnClickListener != null) {
+                    mSecondaryActionOnClickListener.accept(mSecondaryActionChecked);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        View firstActionContainer = requireViewByRefId(holder.itemView,
+                R.id.car_ui_first_action_container);
+        View secondActionContainer = requireViewByRefId(holder.itemView,
+                R.id.car_ui_second_action_container);
+        View secondaryAction = requireViewByRefId(holder.itemView,
+                R.id.car_ui_secondary_action);
+        Switch s = requireViewByRefId(holder.itemView,
+                R.id.car_ui_secondary_action_concrete);
+
+        holder.itemView.setFocusable(false);
+        holder.itemView.setClickable(false);
+        firstActionContainer.setOnClickListener(this::performClick);
+        firstActionContainer.setEnabled(isEnabled() || isUxRestricted());
+        firstActionContainer.setFocusable(isEnabled() || isUxRestricted());
+
+        secondActionContainer.setVisibility(mSecondaryActionVisible ? View.VISIBLE : View.GONE);
+        s.setChecked(mSecondaryActionChecked);
+        s.setEnabled(isSecondaryActionEnabled());
+
+        secondaryAction.setOnClickListener(v -> performSecondaryActionClickInternal());
+        secondaryAction.setEnabled(isSecondaryActionEnabled() || isUxRestricted());
+        secondaryAction.setFocusable(isSecondaryActionEnabled() || isUxRestricted());
+    }
+
+    /**
+     * Sets the checked state of the switch in the secondary action space.
+     * @param checked Whether the switch should be checked or not.
+     */
+    public void setSecondaryActionChecked(boolean checked) {
+        mSecondaryActionChecked = checked;
+        notifyChanged();
+    }
+
+    /**
+     * Returns the checked state of the switch in the secondary action space.
+     * @return Whether the switch is checked or not.
+     */
+    public boolean isSecondaryActionChecked() {
+        return mSecondaryActionChecked;
+    }
+
+    /**
+     * Sets the on-click listener of the secondary action button.
+     *
+     * The listener is called with the current checked state of the switch.
+     */
+    public void setOnSecondaryActionClickListener(@Nullable Consumer<Boolean> onClickListener) {
+        mSecondaryActionOnClickListener = onClickListener;
+        notifyChanged();
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionTextPreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionTextPreference.java
new file mode 100644
index 0000000..9542d29
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/CarUiTwoActionTextPreference.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.preference;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.car.ui.R;
+
+import java.util.function.Consumer;
+
+/**
+ * A preference that has a text button that can be pressed independently of pressing the main
+ * body of the preference.
+ */
+public class CarUiTwoActionTextPreference extends CarUiTwoActionBasePreference {
+
+    @Nullable
+    protected Runnable mSecondaryActionOnClickListener;
+    @Nullable
+    private CharSequence mSecondaryActionText;
+
+    public CarUiTwoActionTextPreference(Context context,
+            AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public CarUiTwoActionTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public CarUiTwoActionTextPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public CarUiTwoActionTextPreference(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void init(@Nullable AttributeSet attrs) {
+        super.init(attrs);
+
+        TypedArray a = getContext().obtainStyledAttributes(attrs,
+                R.styleable.CarUiTwoActionTextPreference);
+        int actionStyle = 0;
+        try {
+            actionStyle = a.getInteger(
+                    R.styleable.CarUiTwoActionTextPreference_secondaryActionStyle, 0);
+            mSecondaryActionText = a.getString(
+                    R.styleable.CarUiTwoActionTextPreference_secondaryActionText);
+        } finally {
+            a.recycle();
+        }
+
+        setLayoutResourceInternal(actionStyle == 0
+                ? R.layout.car_ui_preference_two_action_text
+                : R.layout.car_ui_preference_two_action_text_borderless);
+    }
+
+    @Override
+    protected void performSecondaryActionClickInternal() {
+        if (isSecondaryActionEnabled()) {
+            if (isUxRestricted()) {
+                Consumer<Preference> restrictedListener = getOnClickWhileRestrictedListener();
+                if (restrictedListener != null) {
+                    restrictedListener.accept(this);
+                }
+            } else if (mSecondaryActionOnClickListener != null) {
+                mSecondaryActionOnClickListener.run();
+            }
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        View firstActionContainer = requireViewByRefId(holder.itemView,
+                R.id.car_ui_first_action_container);
+        View secondActionContainer = requireViewByRefId(holder.itemView,
+                R.id.car_ui_second_action_container);
+        Button secondaryButton = requireViewByRefId(holder.itemView,
+                R.id.car_ui_secondary_action);
+
+        holder.itemView.setFocusable(false);
+        holder.itemView.setClickable(false);
+        firstActionContainer.setOnClickListener(this::performClick);
+        firstActionContainer.setEnabled(isEnabled());
+        firstActionContainer.setFocusable(isEnabled());
+
+        secondActionContainer.setVisibility(mSecondaryActionVisible ? View.VISIBLE : View.GONE);
+        secondaryButton.setText(mSecondaryActionText);
+        secondaryButton.setOnClickListener(v -> performSecondaryActionClickInternal());
+        secondaryButton.setEnabled(isSecondaryActionEnabled());
+        secondaryButton.setFocusable(isSecondaryActionEnabled());
+    }
+
+    @Nullable
+    public CharSequence getSecondaryActionText() {
+        return mSecondaryActionText;
+    }
+
+    /**
+     * Sets the title of the secondary action button.
+     *
+     * @param title The text to display on the secondary action.
+     */
+    public void setSecondaryActionText(@Nullable CharSequence title) {
+        mSecondaryActionText = title;
+        notifyChanged();
+    }
+
+    /**
+     * Sets the title of the secondary action button.
+     *
+     * @param resid A string resource of the text to display on the secondary action.
+     */
+    public void setSecondaryActionText(@StringRes int resid) {
+        setSecondaryActionText(getContext().getString(resid));
+    }
+
+    /**
+     * Sets the on-click listener of the secondary action button.
+     */
+    public void setOnSecondaryActionClickListener(@Nullable Runnable onClickListener) {
+        mSecondaryActionOnClickListener = onClickListener;
+        notifyChanged();
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/DisabledPreferenceCallback.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/DisabledPreferenceCallback.java
index ea3585b..f6926be 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/DisabledPreferenceCallback.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/DisabledPreferenceCallback.java
@@ -16,20 +16,26 @@
 
 package com.android.car.ui.preference;
 
+import android.util.Log;
+
 import androidx.annotation.NonNull;
 
 /**
  * Interface for preferences to handle clicks when its disabled.
+ *
+ * @deprecated use {@link UxRestrictablePreference} instead
  */
-public interface DisabledPreferenceCallback {
-
-    /**
-     * Sets if the ripple effect should be shown on disabled preference.
-     */
-    default void setShouldShowRippleOnDisabledPreference(boolean showRipple) {}
+@Deprecated
+public interface DisabledPreferenceCallback extends UxRestrictablePreference {
 
     /**
      * Sets the message to be displayed when the disabled preference is clicked.
+     *
+     * @deprecated Use {@link UxRestrictablePreference#setUxRestricted(boolean)} instead.
      */
-    default void setMessageToShowWhenDisabledPreferenceClicked(@NonNull String message) {}
+    @Deprecated
+    default void setMessageToShowWhenDisabledPreferenceClicked(@NonNull String message) {
+        Log.w("carui",
+                "setMessageToShowWhenDisabledPreferenceClicked is deprecated, and does nothing!");
+    }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java
index b9ad789..12ed526 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/ListPreferenceFragment.java
@@ -58,6 +58,7 @@
     private CarUiContentListItem mSelectedItem;
     private int mSelectedIndex = -1;
     private boolean mFullScreen;
+    private boolean mUseInstantPreferenceChangeCallback;
 
     /**
      * Returns a new instance of {@link ListPreferenceFragment} for the {@link ListPreference} with
@@ -90,6 +91,8 @@
         super.onViewCreated(view, savedInstanceState);
         final CarUiRecyclerView carUiRecyclerView = CarUiUtils.requireViewByRefId(view, R.id.list);
         mFullScreen = requireArguments().getBoolean(ARG_FULLSCREEN, true);
+        mUseInstantPreferenceChangeCallback =
+                getResources().getBoolean(R.bool.car_ui_preference_list_instant_change_callback);
         ToolbarController toolbar = mFullScreen ? CarUi.getToolbar(getActivity()) : null;
 
         // TODO(b/150230923) remove the code for the old toolbar height change when apps are ready
@@ -153,12 +156,21 @@
             }
 
             item.setOnCheckedChangeListener((listItem, isChecked) -> {
+                if (!isChecked) {
+                    // Previously selected item is unchecked now, no further processing is needed.
+                    return;
+                }
+
                 if (mSelectedItem != null) {
                     mSelectedItem.setChecked(false);
                     adapter.notifyItemChanged(listItems.indexOf(mSelectedItem));
                 }
                 mSelectedItem = listItem;
                 mSelectedIndex = listItems.indexOf(mSelectedItem);
+
+                if (mUseInstantPreferenceChangeCallback) {
+                    updatePreference();
+                }
             });
 
             listItems.add(item);
@@ -179,7 +191,10 @@
     @Override
     public void onStop() {
         super.onStop();
-        updatePreference();
+
+        if (!mUseInstantPreferenceChangeCallback) {
+            updatePreference();
+        }
     }
 
     private void updatePreference() {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
index f9cf8a2..a93bad6 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
@@ -59,6 +59,7 @@
     private Set<String> mNewValues;
     private ToolbarController mToolbar;
     private boolean mFullScreen;
+    private boolean mUseInstantPreferenceChangeCallback;
 
     /**
      * Returns a new instance of {@link MultiSelectListPreferenceFragment} for the {@link
@@ -91,6 +92,8 @@
         super.onViewCreated(view, savedInstanceState);
         final CarUiRecyclerView recyclerView = CarUiUtils.requireViewByRefId(view, R.id.list);
         mFullScreen = requireArguments().getBoolean(ARG_FULLSCREEN, true);
+        mUseInstantPreferenceChangeCallback =
+                getResources().getBoolean(R.bool.car_ui_preference_list_instant_change_callback);
         mToolbar = mFullScreen ? CarUi.getToolbar(requireActivity()) : null;
 
         // TODO(b/150230923) remove the code for the old toolbar height change when apps are ready
@@ -158,6 +161,10 @@
                 } else {
                     mNewValues.remove(entryValue);
                 }
+
+                if (mUseInstantPreferenceChangeCallback) {
+                    updatePreference();
+                }
             });
 
             listItems.add(item);
@@ -179,7 +186,9 @@
     @Override
     public void onStop() {
         super.onStop();
-        updatePreference();
+        if (!mUseInstantPreferenceChangeCallback) {
+            updatePreference();
+        }
     }
 
     private void updatePreference() {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/UxRestrictablePreference.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/UxRestrictablePreference.java
new file mode 100644
index 0000000..4774c2c
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/preference/UxRestrictablePreference.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 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.android.car.ui.preference;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import java.util.function.Consumer;
+
+/**
+ * An interface for preferences that can be ux restricted.
+ *
+ * A ux restricted preference will be displayed differently to indicate such, and will
+ * display a toast message informing the user they cannot click it when they try to.
+ */
+public interface UxRestrictablePreference {
+
+    /** Sets this preference as ux restricted or not */
+    void setUxRestricted(boolean restricted);
+
+    /** Returns if this preference is currently ux restricted */
+    boolean isUxRestricted();
+
+    /** Sets a listener to be called if the preference is clicked while it is ux restricted */
+    void setOnClickWhileRestrictedListener(@Nullable Consumer<Preference> listener);
+
+    /** Gets the listener to be called if the preference is clicked while it is ux restricted */
+    @Nullable
+    Consumer<Preference> getOnClickWhileRestrictedListener();
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
index a45b1e8..c452ee2 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
@@ -170,22 +170,25 @@
 
         ListItemViewHolder(@NonNull View itemView) {
             super(itemView);
-            mTitle = requireViewByRefId(itemView, R.id.title);
-            mBody = requireViewByRefId(itemView, R.id.body);
-            mIcon = requireViewByRefId(itemView, R.id.icon);
-            mContentIcon = requireViewByRefId(itemView, R.id.content_icon);
-            mAvatarIcon = requireViewByRefId(itemView, R.id.avatar_icon);
-            mIconContainer = requireViewByRefId(itemView, R.id.icon_container);
-            mActionContainer = requireViewByRefId(itemView, R.id.action_container);
-            mActionDivider = requireViewByRefId(itemView, R.id.action_divider);
-            mSwitch = requireViewByRefId(itemView, R.id.switch_widget);
-            mCheckBox = requireViewByRefId(itemView, R.id.checkbox_widget);
-            mRadioButton = requireViewByRefId(itemView, R.id.radio_button_widget);
-            mSupplementalIcon = requireViewByRefId(itemView, R.id.supplemental_icon);
-            mReducedTouchInterceptor = requireViewByRefId(itemView, R.id.reduced_touch_interceptor);
-            mTouchInterceptor = requireViewByRefId(itemView, R.id.touch_interceptor);
+            mTitle = requireViewByRefId(itemView, R.id.car_ui_list_item_title);
+            mBody = requireViewByRefId(itemView, R.id.car_ui_list_item_body);
+            mIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_icon);
+            mContentIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_content_icon);
+            mAvatarIcon = requireViewByRefId(itemView, R.id.car_ui_list_item_avatar_icon);
+            mIconContainer = requireViewByRefId(itemView, R.id.car_ui_list_item_icon_container);
+            mActionContainer = requireViewByRefId(itemView, R.id.car_ui_list_item_action_container);
+            mActionDivider = requireViewByRefId(itemView, R.id.car_ui_list_item_action_divider);
+            mSwitch = requireViewByRefId(itemView, R.id.car_ui_list_item_switch_widget);
+            mCheckBox = requireViewByRefId(itemView, R.id.car_ui_list_item_checkbox_widget);
+            mRadioButton = requireViewByRefId(itemView, R.id.car_ui_list_item_radio_button_widget);
+            mSupplementalIcon = requireViewByRefId(itemView,
+                    R.id.car_ui_list_item_supplemental_icon);
+            mReducedTouchInterceptor = requireViewByRefId(itemView,
+                    R.id.car_ui_list_item_reduced_touch_interceptor);
+            mTouchInterceptor = requireViewByRefId(itemView,
+                    R.id.car_ui_list_item_touch_interceptor);
             mActionContainerTouchInterceptor = requireViewByRefId(itemView,
-                    R.id.action_container_touch_interceptor);
+                    R.id.car_ui_list_item_action_container_touch_interceptor);
         }
 
         void bind(@NonNull CarUiContentListItem item) {
@@ -371,8 +374,8 @@
 
         HeaderViewHolder(@NonNull View itemView) {
             super(itemView);
-            mTitle = requireViewByRefId(itemView, R.id.title);
-            mBody = requireViewByRefId(itemView, R.id.body);
+            mTitle = requireViewByRefId(itemView, R.id.car_ui_list_item_title);
+            mBody = requireViewByRefId(itemView, R.id.car_ui_list_item_body);
         }
 
         private void bind(@NonNull CarUiHeaderListItem item) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
index d94fbf3..3cf696d 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -77,7 +77,6 @@
     private String mScrollBarClass;
     private int mScrollBarPaddingTop;
     private int mScrollBarPaddingBottom;
-
     @Nullable
     private ScrollBar mScrollBar;
 
@@ -101,7 +100,7 @@
     @Nullable
     private Rect mContainerPaddingRelative;
     @Nullable
-    private LinearLayout mContainer;
+    private ViewGroup mContainer;
 
     // Set to true when when styled attributes are read and initialized.
     private boolean mIsInitialized;
@@ -249,7 +248,7 @@
             return;
         }
 
-        mContainer = new LinearLayout(getContext());
+        mContainer = new FrameLayout(getContext());
 
         setVerticalScrollBarEnabled(false);
         setHorizontalScrollBarEnabled(false);
@@ -302,7 +301,8 @@
         boolean rotaryScrollEnabled = styledAttributes != null && styledAttributes.getBoolean(
                 R.styleable.CarUiRecyclerView_rotaryScrollEnabled, /* defValue=*/ false);
         if (rotaryScrollEnabled) {
-            int orientation = styledAttributes.getInt(R.styleable.RecyclerView_android_orientation,
+            int orientation = styledAttributes.getInt(
+                    R.styleable.CarUiRecyclerView_android_orientation,
                     LinearLayout.VERTICAL);
             CarUiUtils.setRotaryScrollEnabled(
                     this, /* isVertical= */ orientation == LinearLayout.VERTICAL);
@@ -408,6 +408,15 @@
      * the recycler view was set with the same layout params.
      */
     private void installExternalScrollBar() {
+        if (mContainer.getParent() != null) {
+            // We've already installed the parent container.
+            // onAttachToWindow() can be called multiple times, but on the second time
+            // we will crash if we try to add mContainer as a child of a view again while
+            // it already has a parent.
+            return;
+        }
+
+        mContainer.removeAllViews();
         LayoutInflater inflater = LayoutInflater.from(getContext());
         inflater.inflate(R.layout.car_ui_recycler_view, mContainer, true);
         mContainer.setVisibility(mContainerVisibility);
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java
index 42554bf..0199d2d 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/recyclerview/DefaultScrollBar.java
@@ -28,6 +28,7 @@
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.recyclerview.widget.OrientationHelper;
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -224,7 +225,7 @@
         // Sets the size of the thumb and request a redraw if needed.
         ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
 
-        if (lp.height != thumbLength) {
+        if (lp.height != thumbLength || thumbLength < mScrollThumb.getHeight()) {
             lp.height = thumbLength;
             mScrollThumb.requestLayout();
         }
@@ -500,7 +501,33 @@
         if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
             mScrollView.setVisibility(View.INVISIBLE);
         } else {
-            mScrollView.setVisibility(View.VISIBLE);
+            OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
+            int screenSize = orientationHelper.getTotalSpace();
+            int touchTargetSize = (int) getRecyclerView().getContext().getResources()
+                    .getDimension(R.dimen.car_ui_touch_target_size);
+            ConstraintLayout.LayoutParams upButtonLayoutParam =
+                    (ConstraintLayout.LayoutParams) mUpButton.getLayoutParams();
+            int upButtonMargin = upButtonLayoutParam.topMargin
+                    + upButtonLayoutParam.bottomMargin;
+            ConstraintLayout.LayoutParams downButtonLayoutParam =
+                    (ConstraintLayout.LayoutParams) mDownButton.getLayoutParams();
+            int downButtonMargin = downButtonLayoutParam.topMargin
+                    + downButtonLayoutParam.bottomMargin;
+            int margin = upButtonMargin + downButtonMargin;
+            if (screenSize < 2 * touchTargetSize + margin) {
+                mScrollView.setVisibility(View.INVISIBLE);
+            } else {
+                ConstraintLayout.LayoutParams trackLayoutParam =
+                        (ConstraintLayout.LayoutParams) mScrollTrack.getLayoutParams();
+                int trackMargin = trackLayoutParam.topMargin
+                        + trackLayoutParam.bottomMargin;
+                margin += trackMargin;
+                if (screenSize < 3 * touchTargetSize + margin) {
+                    mScrollTrack.setVisibility(View.INVISIBLE);
+                    mScrollThumb.setVisibility(View.INVISIBLE);
+                }
+                mScrollView.setVisibility(View.VISIBLE);
+            }
         }
 
         if (layoutManager == null) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java
index 1f1a07c..612a4d8 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/CarUiEditText.java
@@ -23,6 +23,7 @@
 import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.SEARCH_RESULT_ITEM_ID_LIST;
 import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.SEARCH_RESULT_SUPPLEMENTAL_ICON_ID_LIST;
 import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_CLEAR_DATA_ACTION;
+import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -70,6 +71,12 @@
             setText("");
         }
 
+        if (WIDE_SCREEN_POST_LOAD_SEARCH_RESULTS_ACTION.equals(action)) {
+            for (PrivateImeCommandCallback listener : mPrivateImeCommandCallback) {
+                listener.onPostLoadSearchResults();
+            }
+        }
+
         if (data == null || mPrivateImeCommandCallback == null) {
             return false;
         }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java
index 27f8140..5645b0d 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItem.java
@@ -73,6 +73,17 @@
     private boolean mIsVisible;
     private boolean mIsActivated;
 
+    @SuppressWarnings("FieldCanBeLocal") // Used with weak references
+    private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mUxRestrictionsListener =
+            uxRestrictions -> {
+                boolean wasRestricted = isRestricted();
+                mCurrentRestrictions = uxRestrictions;
+
+                if (isRestricted() != wasRestricted) {
+                    update();
+                }
+            };
+
     private MenuItem(Builder builder) {
         mContext = builder.mContext;
         mId = builder.mId;
@@ -92,7 +103,7 @@
         mIsPrimary = builder.mIsPrimary;
         mUxRestrictions = builder.mUxRestrictions;
 
-        mCurrentRestrictions = CarUxRestrictionsUtil.getInstance(mContext).getCurrentRestrictions();
+        CarUxRestrictionsUtil.getInstance(mContext).register(mUxRestrictionsListener);
     }
 
     private void update() {
@@ -239,15 +250,6 @@
         update();
     }
 
-    /* package */ void setCarUxRestrictions(CarUxRestrictions restrictions) {
-        boolean wasRestricted = isRestricted();
-        mCurrentRestrictions = restrictions;
-
-        if (isRestricted() != wasRestricted) {
-            update();
-        }
-    }
-
     /* package */ boolean isRestricted() {
         return CarUxRestrictionsUtil.isRestricted(mUxRestrictions, mCurrentRestrictions);
     }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java
index ca3364b..6d5f572 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/MenuItemRenderer.java
@@ -18,7 +18,6 @@
 import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
 
 import android.app.Activity;
-import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
@@ -85,10 +84,6 @@
         }
     }
 
-    void setCarUxRestrictions(CarUxRestrictions restrictions) {
-        mMenuItem.setCarUxRestrictions(restrictions);
-    }
-
     @Override
     public void onMenuItemChanged(MenuItem changedItem) {
         updateView();
@@ -190,7 +185,7 @@
         if (view instanceof ImageView) {
             ((ImageView) view).setImageState(drawableState, true);
         } else if (view instanceof DrawableStateView) {
-            ((DrawableStateView) view).setDrawableState(drawableState);
+            ((DrawableStateView) view).setExtraDrawableState(drawableState, null);
         }
 
         if (view instanceof ViewGroup) {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java
index 49e7fa4..48583c7 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/PrivateImeCommandCallback.java
@@ -49,4 +49,9 @@
      * content area.
      */
     void onSurfaceInfo(int displayId, IBinder binder, int height, int width);
+
+    /**
+     * Called when the search results are read and displayed from the content provider by IME.
+     */
+    void onPostLoadSearchResults();
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java
index dd6f056..8b0b656 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/SearchView.java
@@ -86,6 +86,7 @@
     private final Handler mHandler = new Handler(Looper.getMainLooper());
 
     private SurfaceControlViewHost mSurfaceControlViewHost;
+    private Uri mContentUri;
     private int mSurfaceHeight;
     private int mSurfaceWidth;
     private List<? extends CarUiImeSearchListItem> mWideScreenSearchItemList;
@@ -278,10 +279,10 @@
     private void displaySearchWideScreen() {
         String url = CONTENT + getContext().getPackageName() + SEARCH_RESULTS_PROVIDER + "/"
                 + SEARCH_RESULTS_TABLE_NAME;
-        Uri contentUri = Uri.parse(url);
+        mContentUri = Uri.parse(url);
         mIdToListItem.clear();
         // clear the table.
-        getContext().getContentResolver().delete(contentUri, null, null);
+        getContext().getContentResolver().delete(mContentUri, null, null);
 
         // mWideScreenImeContentAreaView will only be set when running in widescreen mode and
         // apps allowed by OEMs are trying to set their own view. In that case we did not want to
@@ -313,7 +314,7 @@
                     item.getTitle() != null ? item.getTitle().toString() : null);
             values.put(SearchResultsProvider.SUBTITLE,
                     item.getBody() != null ? item.getBody().toString() : null);
-            getContext().getContentResolver().insert(contentUri, values);
+            getContext().getContentResolver().insert(mContentUri, values);
             mIdToListItem.put(idString, item);
             id++;
         }
@@ -454,5 +455,12 @@
             mInputMethodManager.sendAppPrivateCommand(mSearchText,
                     WIDE_SCREEN_ACTION, bundle);
         }
+
+        @Override
+        public void onPostLoadSearchResults() {
+            if (mContentUri != null) {
+                getContext().getContentResolver().delete(mContentUri, null, null);
+            }
+        }
     }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java
index 02b3a1a..e30f5f5 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerImpl.java
@@ -49,7 +49,6 @@
 import com.android.car.ui.recyclerview.CarUiListItem;
 import com.android.car.ui.recyclerview.CarUiListItemAdapter;
 import com.android.car.ui.utils.CarUiUtils;
-import com.android.car.ui.utils.CarUxRestrictionsUtil;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -123,15 +122,6 @@
         setState(getState());
     };
 
-    // Despite the warning, this has to be a field so it's not garbage-collected.
-    // The only other reference to it is a weak reference
-    private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener
-            mOnUxRestrictionsChangedListener = restrictions -> {
-                for (MenuItemRenderer renderer : mMenuItemRenderers) {
-                    renderer.setCarUxRestrictions(restrictions);
-                }
-            };
-
 
     public ToolbarControllerImpl(View view) {
         mContext = view.getContext();
@@ -197,10 +187,6 @@
         setBackgroundShown(true);
 
         mOverflowAdapter = new CarUiListItemAdapter(mUiOverflowItems);
-
-        // This holds weak references so we don't need to unregister later
-        CarUxRestrictionsUtil.getInstance(getContext())
-                .register(mOnUxRestrictionsChangedListener);
     }
 
     private Context getContext() {
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
index ffa191b..01651fd 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/CarUiUtils.java
@@ -33,7 +33,6 @@
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.view.View;
-import android.view.ViewGroup;
 
 import androidx.annotation.DimenRes;
 import androidx.annotation.IdRes;
@@ -41,7 +40,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.StyleRes;
 import androidx.annotation.UiThread;
-import androidx.core.view.ViewCompat;
 
 import java.lang.reflect.Method;
 
@@ -106,70 +104,6 @@
     }
 
     /**
-     * Updates the preference view enabled state. If the view is disabled we just disable the child
-     * of preference like TextView, ImageView. The preference itself is always enabled to get the
-     * click events. Ripple effect in background is also removed by default. If the ripple is
-     * needed see
-     * {@link IDisabledPreferenceCallback#setShouldShowRippleOnDisabledPreference(boolean)}
-     */
-    public static Drawable setPreferenceViewEnabled(boolean viewEnabled, View itemView,
-            Drawable background, boolean shouldShowRippleOnDisabledPreference) {
-        if (viewEnabled) {
-            if (background != null) {
-                ViewCompat.setBackground(itemView, background);
-            }
-            setChildViewsEnabled(itemView, true, false);
-        } else {
-            itemView.setEnabled(true);
-            if (background == null) {
-                // store the original background.
-                background = itemView.getBackground();
-            }
-            updateRippleStateOnDisabledPreference(false, shouldShowRippleOnDisabledPreference,
-                    background, itemView);
-            setChildViewsEnabled(itemView, false, true);
-        }
-        return background;
-    }
-
-    /**
-     * Sets the enabled state on the views of the preference. If the view is being disabled we want
-     * only child views of preference to be disabled.
-     */
-    private static void setChildViewsEnabled(View view, boolean enabled, boolean isRootView) {
-        if (!isRootView) {
-            view.setEnabled(enabled);
-        }
-        if (view instanceof ViewGroup) {
-            ViewGroup grp = (ViewGroup) view;
-            for (int index = 0; index < grp.getChildCount(); index++) {
-                setChildViewsEnabled(grp.getChildAt(index), enabled, false);
-            }
-        }
-    }
-
-    /**
-     * Updates the ripple state on the given preference.
-     *
-     * @param isEnabled                            whether the preference is enabled or not
-     * @param shouldShowRippleOnDisabledPreference should ripple be displayed when the preference is
-     *                                             clicked
-     * @param background                           drawable that represents the ripple
-     * @param preference                           preference on which drawable will be applied
-     */
-    public static void updateRippleStateOnDisabledPreference(boolean isEnabled,
-            boolean shouldShowRippleOnDisabledPreference, Drawable background, View preference) {
-        if (isEnabled || preference == null) {
-            return;
-        }
-        if (shouldShowRippleOnDisabledPreference && background != null) {
-            ViewCompat.setBackground(preference, background);
-        } else {
-            ViewCompat.setBackground(preference, null);
-        }
-    }
-
-    /**
      * Enables rotary scrolling for {@code view}, either vertically (if {@code isVertical} is true)
      * or horizontally (if {@code isVertical} is false). With rotary scrolling enabled, rotating the
      * rotary controller will scroll rather than moving the focus when moving the focus would cause
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/DirectManipulationHelper.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/DirectManipulationHelper.java
index 184e522..09d1662 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/DirectManipulationHelper.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/DirectManipulationHelper.java
@@ -26,16 +26,18 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
 /** Helper class to toggle direct manipulation mode. */
 public final class DirectManipulationHelper {
 
     /**
-     * StateDescription for a {@link View} to support direct manipulation mode. It's also used as
+     * ContentDescription for a {@link View} to support direct manipulation mode. It's also used as
      * class name of {@link AccessibilityEvent} to indicate that the AccessibilityEvent represents
      * a request to toggle direct manipulation mode.
      */
-    private static final String DIRECT_MANIPULATION =
+    @VisibleForTesting
+    public static final String DIRECT_MANIPULATION =
             "com.android.car.ui.utils.DIRECT_MANIPULATION";
 
     /** This is a utility class. */
@@ -77,7 +79,7 @@
     /** Returns whether the given {@code node} supports rotate directly. */
     @TargetApi(R)
     public static boolean supportRotateDirectly(@NonNull AccessibilityNodeInfo node) {
-        return TextUtils.equals(DIRECT_MANIPULATION, node.getStateDescription());
+        return TextUtils.equals(DIRECT_MANIPULATION, node.getContentDescription());
     }
 
     /**
@@ -98,7 +100,7 @@
      */
     @TargetApi(R)
     public static void setSupportsRotateDirectly(@NonNull View view, boolean enable) {
-        view.setStateDescription(enable ? DIRECT_MANIPULATION : null);
+        view.setContentDescription(enable ? DIRECT_MANIPULATION : null);
     }
 
     /**
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
index 62c189f..bc5a583 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/utils/ViewUtils.java
@@ -35,6 +35,8 @@
 
 import com.android.car.ui.FocusArea;
 import com.android.car.ui.FocusParkingView;
+import com.android.car.ui.R;
+import com.android.car.ui.uxr.DrawableStateView;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -47,31 +49,39 @@
  */
 public final class ViewUtils {
 
+    private static int[] sRestrictedState;
+
     /**
      * No view is focused, the focused view is not shown, or the focused view is a FocusParkingView.
      */
-    public static final int NO_FOCUS = 1;
+    @VisibleForTesting
+    static final int NO_FOCUS = 1;
 
     /** A scrollable container is focused. */
-    public static final int SCROLLABLE_CONTAINER_FOCUS = 2;
+    @VisibleForTesting
+    static final int SCROLLABLE_CONTAINER_FOCUS = 2;
 
     /**
      * A regular view is focused. A regular View is a View that is neither a FocusParkingView nor a
      * scrollable container.
      */
-    public static final int REGULAR_FOCUS = 3;
+    @VisibleForTesting
+    static final int REGULAR_FOCUS = 3;
 
     /**
-     * An implicit default focus view (i.e., the first focusable item in a scrollable container) is
-     * focused.
+     * An implicit default focus view (i.e., the selected item or the first focusable item in a
+     * scrollable container) is focused.
      */
-    public static final int IMPLICIT_DEFAULT_FOCUS = 4;
+    @VisibleForTesting
+    static final int IMPLICIT_DEFAULT_FOCUS = 4;
 
     /** The {@code app:defaultFocus} view is focused. */
-    public static final int DEFAULT_FOCUS = 5;
+    @VisibleForTesting
+    static final int DEFAULT_FOCUS = 5;
 
     /** The {@code android:focusedByDefault} view is focused. */
-    public static final int FOCUSED_BY_DEFAULT = 6;
+    @VisibleForTesting
+    static final int FOCUSED_BY_DEFAULT = 6;
 
     /**
      * Focus level of a view. When adjusting the focus, the view with the highest focus level will
@@ -80,7 +90,7 @@
     @IntDef(flag = true, value = {NO_FOCUS, SCROLLABLE_CONTAINER_FOCUS, REGULAR_FOCUS,
             IMPLICIT_DEFAULT_FOCUS, DEFAULT_FOCUS, FOCUSED_BY_DEFAULT})
     @Retention(RetentionPolicy.SOURCE)
-    public @interface FocusLevel {
+    private @interface FocusLevel {
     }
 
     /** This is a utility class. */
@@ -158,8 +168,24 @@
      * @return whether the view is focused
      */
     public static boolean adjustFocus(@NonNull View root, @Nullable View currentFocus) {
-        @FocusLevel int level = getFocusLevel(currentFocus);
-        return adjustFocus(root, level);
+        @FocusLevel int currentLevel = getFocusLevel(currentFocus);
+        return adjustFocus(root, currentLevel, /* cachedFocusedView= */ null,
+                /* defaultFocusOverridesHistory= */ false);
+    }
+
+    /**
+     * If the {@code currentFocus}'s FocusLevel is lower than REGULAR_FOCUS, adjusts focus within
+     * {@code root}. See {@link #adjustFocus(View, int)}. Otherwise no-op.
+     *
+     * @return whether the focus has changed
+     */
+    public static boolean initFocus(@NonNull View root, @Nullable View currentFocus) {
+        @FocusLevel int currentLevel = getFocusLevel(currentFocus);
+        if (currentLevel >= REGULAR_FOCUS) {
+            return false;
+        }
+        return adjustFocus(root, currentLevel, /* cachedFocusedView= */ null,
+                /* defaultFocusOverridesHistory= */ false);
     }
 
     /**
@@ -168,7 +194,42 @@
      *
      * @return whether the view is focused
      */
-    public static boolean adjustFocus(@NonNull View root, @FocusLevel int currentLevel) {
+    @VisibleForTesting
+    static boolean adjustFocus(@NonNull View root, @FocusLevel int currentLevel) {
+        return adjustFocus(root, currentLevel, /* cachedFocusedView= */ null,
+                /* defaultFocusOverridesHistory= */ false);
+    }
+
+    /**
+     * Searches the {@code root}'s descendants for a view with the highest {@link FocusLevel} and
+     * focuses on it or the {@code cachedFocusedView}.
+     *
+     * @return whether the view is focused
+     */
+    public static boolean adjustFocus(@NonNull View root,
+            @Nullable View cachedFocusedView,
+            boolean defaultFocusOverridesHistory) {
+        return adjustFocus(root, NO_FOCUS, cachedFocusedView, defaultFocusOverridesHistory);
+    }
+
+    /**
+     * Searches the {@code root}'s descendants for a view with the highest {@link FocusLevel}. If
+     * the view's FocusLevel is higher than {@code currentLevel}, focuses on the view or {@code
+     * cachedFocusedView}.
+     *
+     * @return whether the view is focused
+     */
+    private static boolean adjustFocus(@NonNull View root,
+            @FocusLevel int currentLevel,
+            @Nullable View cachedFocusedView,
+            boolean defaultFocusOverridesHistory) {
+        // If the previously focused view has higher priority than the default focus, try to focus
+        // on the previously focused view.
+        if (!defaultFocusOverridesHistory && requestFocus(cachedFocusedView)) {
+            return true;
+        }
+
+        // Try to focus on the default focus view.
         if (currentLevel < FOCUSED_BY_DEFAULT && focusOnFocusedByDefaultView(root)) {
             return true;
         }
@@ -178,6 +239,14 @@
         if (currentLevel < IMPLICIT_DEFAULT_FOCUS && focusOnImplicitDefaultFocusView(root)) {
             return true;
         }
+
+        // If the previously focused view has lower priority than the default focus, try to focus
+        // on the previously focused view.
+        if (defaultFocusOverridesHistory && requestFocus(cachedFocusedView)) {
+            return true;
+        }
+
+        // Try to focus on other views with low focus levels.
         if (currentLevel < REGULAR_FOCUS && focusOnFirstRegularView(root)) {
             return true;
         }
@@ -215,8 +284,8 @@
     }
 
     /**
-     * Returns whether the {@code view} is an implicit default focus view, i.e., the first focusable
-     * item in a rotary container.
+     * Returns whether the {@code view} is an implicit default focus view, i.e., the selected
+     * item or the first focusable item in a rotary container.
      */
     @VisibleForTesting
     static boolean isImplicitDefaultFocusView(@NonNull View view) {
@@ -233,7 +302,8 @@
         if (rotaryContainer == null) {
             return false;
         }
-        return findFirstFocusableDescendant(rotaryContainer) == view;
+        return findFirstSelectedFocusableDescendant(rotaryContainer) == view
+                || findFirstFocusableDescendant(rotaryContainer) == view;
     }
 
     private static boolean isRotaryContainer(@NonNull View view) {
@@ -358,15 +428,21 @@
 
     /**
      * Searches the {@code view} and its descendants in depth first order, and returns the first
-     * implicit default focus view, i.e., the first focusable item in the first rotary container.
-     * Returns null if not found.
+     * implicit default focus view, i.e., the selected item or the first focusable item in the
+     * first rotary container. Returns null if not found.
      */
     @VisibleForTesting
     @Nullable
     static View findImplicitDefaultFocusView(@NonNull View view) {
         View rotaryContainer = findRotaryContainer(view);
-        return rotaryContainer == null
-                ? null
+        if (rotaryContainer == null) {
+            return null;
+        }
+
+        View selectedItem = findFirstSelectedFocusableDescendant(rotaryContainer);
+
+        return selectedItem != null
+                ? selectedItem
                 : findFirstFocusableDescendant(rotaryContainer);
     }
 
@@ -383,6 +459,18 @@
     }
 
     /**
+     * Searches the {@code view}'s descendants in depth first order, and returns the first view
+     * that is selected and can take focus, or null if not found.
+     */
+    @VisibleForTesting
+    @Nullable
+    static View findFirstSelectedFocusableDescendant(@NonNull View view) {
+        return depthFirstSearch(view,
+                /* targetPredicate= */ v -> v != view && v.isSelected() && canTakeFocus(v),
+                /* skipPredicate= */ v -> !v.isShown());
+    }
+
+    /**
      * Searches the {@code view} and its descendants in depth first order, and returns the first
      * rotary container shown on the screen. Returns null if not found.
      */
@@ -431,4 +519,39 @@
                 // descendants. We focus on it so that the rotary controller can scroll it.
                 && (!isScrollableContainer(view) || findFirstFocusableDescendant(view) == null);
     }
+
+    /**
+     * Traverses the view hierarchy, and whenever it sees a {@link DrawableStateView}, adds
+     * state_ux_restricted to it.
+     *
+     * Note that this will remove any other drawable states added by other calls to
+     * {@link DrawableStateView#setExtraDrawableState(int[], int[])}
+     */
+    public static void makeAllViewsUxRestricted(@Nullable View view, boolean restricted) {
+        if (view instanceof DrawableStateView) {
+            if (sRestrictedState == null) {
+                int androidStateUxRestricted = view.getResources()
+                            .getIdentifier("state_ux_restricted", "attr", "android");
+
+                if (androidStateUxRestricted == 0) {
+                    sRestrictedState = new int[] { R.attr.state_ux_restricted };
+                } else {
+                    sRestrictedState = new int[] {
+                        R.attr.state_ux_restricted,
+                        androidStateUxRestricted
+                    };
+                }
+            }
+
+            ((DrawableStateView) view).setExtraDrawableState(
+                    restricted ? sRestrictedState : null, null);
+        }
+
+        if (view instanceof ViewGroup) {
+            ViewGroup vg = (ViewGroup) view;
+            for (int i = 0; i < vg.getChildCount(); i++) {
+                makeAllViewsUxRestricted(vg.getChildAt(i), restricted);
+            }
+        }
+    }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateButton.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateButton.java
index 9e5e1ce..bf7fcb1 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateButton.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateButton.java
@@ -26,8 +26,7 @@
  * such as ux restriction.
  */
 public class DrawableStateButton extends Button implements DrawableStateView {
-
-    private int[] mState;
+    private DrawableStateUtil mUtil;
 
     public DrawableStateButton(Context context) {
         super(context);
@@ -41,24 +40,19 @@
         super(context, attrs, defStyleAttr);
     }
 
-    public DrawableStateButton(
-            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-    }
-
     @Override
-    public void setDrawableState(int[] state) {
-        mState = state;
-        refreshDrawableState();
+    public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
     }
 
     @Override
     public int[] onCreateDrawableState(int extraSpace) {
-        if (mState == null) {
-            return super.onCreateDrawableState(extraSpace);
-        } else {
-            return mergeDrawableStates(
-                    super.onCreateDrawableState(extraSpace + mState.length), mState);
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
         }
+        return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
     }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateConstraintLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateConstraintLayout.java
new file mode 100644
index 0000000..647bfa4
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateConstraintLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.Nullable;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+/**
+ * A {@link ConstraintLayout} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateConstraintLayout extends ConstraintLayout implements DrawableStateView {
+    private DrawableStateUtil mUtil;
+
+    public DrawableStateConstraintLayout(Context context) {
+        super(context);
+    }
+
+    public DrawableStateConstraintLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DrawableStateConstraintLayout(Context context,
+            @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateFrameLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateFrameLayout.java
new file mode 100644
index 0000000..f743043
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateFrameLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link FrameLayout} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateFrameLayout extends FrameLayout implements DrawableStateView {
+    private DrawableStateUtil mUtil;
+
+    public DrawableStateFrameLayout(Context context) {
+        super(context);
+    }
+
+    public DrawableStateFrameLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DrawableStateFrameLayout(Context context,
+            @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateImageView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateImageView.java
new file mode 100644
index 0000000..511da66
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateImageView.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link ImageView} that implements {@link DrawableStateView}, for allowing additional states
+ * such as ux restriction.
+ */
+public class DrawableStateImageView extends ImageView implements DrawableStateView {
+    private DrawableStateUtil mUtil;
+
+    public DrawableStateImageView(Context context) {
+        super(context);
+    }
+
+    public DrawableStateImageView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DrawableStateImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateLinearLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateLinearLayout.java
new file mode 100644
index 0000000..689a32f
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateLinearLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link LinearLayout} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateLinearLayout extends LinearLayout implements DrawableStateView {
+    private DrawableStateUtil mUtil;
+
+    public DrawableStateLinearLayout(Context context) {
+        super(context);
+    }
+
+    public DrawableStateLinearLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DrawableStateLinearLayout(Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateRelativeLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateRelativeLayout.java
new file mode 100644
index 0000000..1c69300
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateRelativeLayout.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link RelativeLayout} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateRelativeLayout extends RelativeLayout implements DrawableStateView {
+    private DrawableStateUtil mUtil;
+
+    public DrawableStateRelativeLayout(Context context) {
+        super(context);
+    }
+
+    public DrawableStateRelativeLayout(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DrawableStateRelativeLayout(Context context,
+            @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateSwitch.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateSwitch.java
index bfa018c..c047592 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateSwitch.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateSwitch.java
@@ -26,7 +26,7 @@
  * such as ux restriction.
  */
 public class DrawableStateSwitch extends Switch implements DrawableStateView {
-    private int[] mState;
+    private DrawableStateUtil mUtil;
 
     public DrawableStateSwitch(Context context) {
         super(context);
@@ -46,18 +46,18 @@
     }
 
     @Override
-    public void setDrawableState(int[] state) {
-        mState = state;
-        refreshDrawableState();
+    public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
     }
 
     @Override
     public int[] onCreateDrawableState(int extraSpace) {
-        if (mState == null) {
-            return super.onCreateDrawableState(extraSpace);
-        } else {
-            return mergeDrawableStates(
-                    super.onCreateDrawableState(extraSpace + mState.length), mState);
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
         }
+        return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
     }
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateTextView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateTextView.java
new file mode 100644
index 0000000..42324fe
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateTextView.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 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.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link TextView} that implements {@link DrawableStateView}, for allowing additional
+ * states such as ux restriction.
+ */
+public class DrawableStateTextView extends TextView implements DrawableStateView {
+    private DrawableStateUtil mUtil;
+
+    public DrawableStateTextView(Context context) {
+        super(context);
+    }
+
+    public DrawableStateTextView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DrawableStateTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        mUtil.setExtraDrawableState(stateToAdd, stateToRemove);
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        if (mUtil == null) {
+            mUtil = new DrawableStateUtil(this);
+        }
+        return mUtil.onCreateDrawableState(extraSpace, space -> super.onCreateDrawableState(space));
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateUtil.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateUtil.java
new file mode 100644
index 0000000..12f05f1
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateUtil.java
@@ -0,0 +1,88 @@
+/*
+ * 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.android.car.ui.uxr;
+
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.function.Function;
+
+/**
+ * This is a utility class designed to make it easier to create a new {@link DrawableStateView}.
+ *
+ * To use, subclass a view and forward it's {@link View#onCreateDrawableState(int)} and
+ * {@link DrawableStateView#setExtraDrawableState(int[], int[])} methods to this object.
+ */
+class DrawableStateUtil implements DrawableStateView {
+
+    private final View mView;
+    @Nullable
+    private int[] mStateToAdd;
+    @Nullable
+    private int[] mStateToRemove;
+
+    DrawableStateUtil(View view) {
+        mView = view;
+    }
+
+    /**
+     * Forward the View's onCreateDrawableState to this method.
+     *
+     * @param extraSpace The extraSpace parameter passed to the View's onCreateDrawableState
+     * @param callSuper  A reference to the View's super.onCreateDrawableState()
+     * @return The intended result of the View's onCreateDrawableState()
+     */
+    public int[] onCreateDrawableState(int extraSpace, Function<Integer, int[]> callSuper) {
+        int[] result;
+        if (mStateToAdd == null) {
+            result = callSuper.apply(extraSpace);
+        } else {
+            result = mergeDrawableStates(
+                    callSuper.apply(extraSpace + mStateToAdd.length), mStateToAdd);
+        }
+
+        if (mStateToRemove != null && mStateToRemove.length != 0) {
+            result = Arrays.stream(result)
+                    .filter(state -> Arrays.stream(mStateToRemove).noneMatch(
+                            toRemove -> state == toRemove))
+                    .toArray();
+        }
+
+        return result;
+    }
+
+    /**
+     * Forward your View's setExtraDrawableState here.
+     */
+    @Override
+    public void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove) {
+        mStateToAdd = stateToAdd;
+        mStateToRemove = stateToRemove;
+        mView.refreshDrawableState();
+    }
+
+    /** Copied from {@link View} */
+    private static int[] mergeDrawableStates(int[] baseState, int[] additionalState) {
+        int i = baseState.length - 1;
+        while (i >= 0 && baseState[i] == 0) {
+            i--;
+        }
+        System.arraycopy(additionalState, 0, baseState, i + 1, additionalState.length);
+        return baseState;
+    }
+}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateView.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateView.java
index a9cdb52..87aa8a9 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateView.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/uxr/DrawableStateView.java
@@ -15,14 +15,22 @@
  */
 package com.android.car.ui.uxr;
 
+import androidx.annotation.Nullable;
+
 /**
- * An Interface to expose a view's drawable state.
+ * An Interface to manipulate a view's drawable state.
  *
- * <p>Used by {@link com.android.car.ui.toolbar.Toolbar Toolbar's}
- * {@link com.android.car.ui.toolbar.MenuItem MenuItems} to make the views display if they are ux
- * restricted.
+ * <p>Used by {@link com.android.car.ui.toolbar.MenuItem MenuItems} to make the views display
+ * if they are ux restricted.
  */
 public interface DrawableStateView {
-    /** Sets the drawable state. This should merge with existing drawable states */
-    void setDrawableState(int[] state);
+    /**
+     * Sets the drawable state. This should merge with existing drawable states
+     *
+     * @param stateToAdd An array of drawable states to add to the view's drawable state, along
+     *                   with any drawable state that would normally be there.
+     * @param stateToRemove An array of drawable states to remove from what would normally be
+     *                      used to display the view.
+     */
+    void setExtraDrawableState(@Nullable int[] stateToAdd, @Nullable int[] stateToRemove);
 }
diff --git a/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml b/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml
index b6f00f0..af550e0 100644
--- a/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res-overlayable/values/overlayable.xml
@@ -1,5 +1,5 @@
 <?xml version='1.0' encoding='UTF-8'?>
-<!--Copyright (C) 2020 The Android Open Source Project
+<!--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.
@@ -80,6 +80,7 @@
       <item type="bool" name="car_ui_ime_wide_screen_aligned_left"/>
       <item type="bool" name="car_ui_ime_wide_screen_allow_app_hide_content_area"/>
       <item type="bool" name="car_ui_list_item_single_line_title"/>
+      <item type="bool" name="car_ui_preference_list_instant_change_callback"/>
       <item type="bool" name="car_ui_preference_list_show_full_screen"/>
       <item type="bool" name="car_ui_preference_show_chevron"/>
       <item type="bool" name="car_ui_scrollbar_enable"/>
@@ -147,6 +148,7 @@
       <item type="dimen" name="car_ui_ime_wide_screen_error_text_size"/>
       <item type="dimen" name="car_ui_ime_wide_screen_input_area_height"/>
       <item type="dimen" name="car_ui_ime_wide_screen_input_area_margin_top"/>
+      <item type="dimen" name="car_ui_ime_wide_screen_input_area_padding_end"/>
       <item type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_left"/>
       <item type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_right"/>
       <item type="dimen" name="car_ui_ime_wide_screen_input_edit_text_size"/>
@@ -309,26 +311,39 @@
       <item type="drawable" name="car_ui_toolbar_menu_item_icon_ripple"/>
       <item type="drawable" name="car_ui_toolbar_search_close_icon"/>
       <item type="drawable" name="car_ui_toolbar_search_search_icon"/>
-      <item type="id" name="action_container"/>
-      <item type="id" name="action_container_touch_interceptor"/>
-      <item type="id" name="action_divider"/>
       <item type="id" name="action_widget_container"/>
-      <item type="id" name="avatar_icon"/>
-      <item type="id" name="body"/>
       <item type="id" name="car_ui_alert_icon"/>
       <item type="id" name="car_ui_alert_subtitle"/>
       <item type="id" name="car_ui_alert_title"/>
       <item type="id" name="car_ui_base_layout_content_container"/>
       <item type="id" name="car_ui_closeKeyboard"/>
       <item type="id" name="car_ui_contentAreaAutomotive"/>
+      <item type="id" name="car_ui_divider"/>
+      <item type="id" name="car_ui_first_action_container"/>
       <item type="id" name="car_ui_focus_area"/>
       <item type="id" name="car_ui_fullscreenArea"/>
       <item type="id" name="car_ui_imeWideScreenInputArea"/>
       <item type="id" name="car_ui_ime_surface"/>
       <item type="id" name="car_ui_inputExtractActionAutomotive"/>
       <item type="id" name="car_ui_inputExtractEditTextContainer"/>
+      <item type="id" name="car_ui_list_item_action_container"/>
+      <item type="id" name="car_ui_list_item_action_container_touch_interceptor"/>
+      <item type="id" name="car_ui_list_item_action_divider"/>
+      <item type="id" name="car_ui_list_item_avatar_icon"/>
+      <item type="id" name="car_ui_list_item_body"/>
+      <item type="id" name="car_ui_list_item_checkbox_widget"/>
+      <item type="id" name="car_ui_list_item_content_icon"/>
       <item type="id" name="car_ui_list_item_end_guideline"/>
+      <item type="id" name="car_ui_list_item_icon"/>
+      <item type="id" name="car_ui_list_item_icon_container"/>
+      <item type="id" name="car_ui_list_item_radio_button_widget"/>
+      <item type="id" name="car_ui_list_item_reduced_touch_interceptor"/>
       <item type="id" name="car_ui_list_item_start_guideline"/>
+      <item type="id" name="car_ui_list_item_supplemental_icon"/>
+      <item type="id" name="car_ui_list_item_switch_widget"/>
+      <item type="id" name="car_ui_list_item_text_container"/>
+      <item type="id" name="car_ui_list_item_title"/>
+      <item type="id" name="car_ui_list_item_touch_interceptor"/>
       <item type="id" name="car_ui_list_limiting_message"/>
       <item type="id" name="car_ui_preference_container_without_widget"/>
       <item type="id" name="car_ui_preference_fragment_container"/>
@@ -338,6 +353,9 @@
       <item type="id" name="car_ui_scrollbar_page_up"/>
       <item type="id" name="car_ui_scrollbar_thumb"/>
       <item type="id" name="car_ui_scrollbar_track"/>
+      <item type="id" name="car_ui_second_action_container"/>
+      <item type="id" name="car_ui_secondary_action"/>
+      <item type="id" name="car_ui_secondary_action_concrete"/>
       <item type="id" name="car_ui_toolbar"/>
       <item type="id" name="car_ui_toolbar_background"/>
       <item type="id" name="car_ui_toolbar_bottom_guideline"/>
@@ -377,17 +395,11 @@
       <item type="id" name="car_ui_wideScreenExtractedTextIcon"/>
       <item type="id" name="car_ui_wideScreenInputArea"/>
       <item type="id" name="car_ui_wideScreenSearchResultList"/>
-      <item type="id" name="checkbox_widget"/>
       <item type="id" name="container"/>
-      <item type="id" name="content_icon"/>
-      <item type="id" name="icon"/>
-      <item type="id" name="icon_container"/>
       <item type="id" name="list"/>
       <item type="id" name="nested_recycler_view_layout"/>
       <item type="id" name="radio_button"/>
-      <item type="id" name="radio_button_widget"/>
       <item type="id" name="recycler_view"/>
-      <item type="id" name="reduced_touch_interceptor"/>
       <item type="id" name="search"/>
       <item type="id" name="seek_bar"/>
       <item type="id" name="seek_bar_text_left"/>
@@ -396,14 +408,9 @@
       <item type="id" name="seekbar"/>
       <item type="id" name="seekbar_value"/>
       <item type="id" name="spinner"/>
-      <item type="id" name="supplemental_icon"/>
-      <item type="id" name="switch_widget"/>
-      <item type="id" name="text_container"/>
       <item type="id" name="textbox"/>
-      <item type="id" name="title"/>
       <item type="id" name="title_template"/>
       <item type="id" name="toolbar"/>
-      <item type="id" name="touch_interceptor"/>
       <item type="integer" name="car_ui_default_max_string_length"/>
       <item type="integer" name="car_ui_focus_area_history_cache_type"/>
       <item type="integer" name="car_ui_focus_area_history_expiration_period_ms"/>
@@ -430,6 +437,10 @@
       <item type="layout" name="car_ui_preference_dropdown"/>
       <item type="layout" name="car_ui_preference_fragment"/>
       <item type="layout" name="car_ui_preference_fragment_with_toolbar"/>
+      <item type="layout" name="car_ui_preference_two_action_icon"/>
+      <item type="layout" name="car_ui_preference_two_action_switch"/>
+      <item type="layout" name="car_ui_preference_two_action_text"/>
+      <item type="layout" name="car_ui_preference_two_action_text_borderless"/>
       <item type="layout" name="car_ui_preference_widget_checkbox"/>
       <item type="layout" name="car_ui_preference_widget_seekbar"/>
       <item type="layout" name="car_ui_preference_widget_switch"/>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml
index 7ac4732..38d1c38 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_header_list_item.xml
@@ -32,7 +32,7 @@
     <!-- Use nested layout as a workaround for a regression in constraintlayout where chains not
     are not rendered correctly when the tail is gone (b/168627311).-->
     <LinearLayout
-        android:id="@+id/text_container"
+        android:id="@+id/car_ui_list_item_text_container"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         app:layout_constraintTop_toTopOf="parent"
@@ -42,7 +42,7 @@
         android:orientation="vertical">
 
         <TextView
-            android:id="@+id/title"
+            android:id="@+id/car_ui_list_item_title"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginStart="@dimen/car_ui_header_list_item_text_start_margin"
@@ -50,7 +50,7 @@
             android:textAppearance="@style/TextAppearance.CarUi.ListItem.Header" />
 
         <TextView
-            android:id="@+id/body"
+            android:id="@+id/car_ui_list_item_body"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginStart="@dimen/car_ui_list_item_text_no_icon_start_margin"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml
index fd7539b..2d68cd8 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_ims_wide_screen_input_view.xml
@@ -62,6 +62,7 @@
                     android:scrollbars="vertical"
                     android:gravity="left|center"
                     android:backgroundTint="@drawable/car_ui_ime_wide_screen_input_area_tint_color"
+                    android:paddingEnd="@dimen/car_ui_ime_wide_screen_input_area_padding_end"
                     android:minLines="1"
                     android:inputType="text"/>
 
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml
index d0f179e..55877e9 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_list_item.xml
@@ -26,7 +26,7 @@
     <!-- The following touch interceptor views are sized to encompass the specific sub-sections of
     the list item view to easily control the bounds of a background ripple effects. -->
     <View
-        android:id="@+id/touch_interceptor"
+        android:id="@+id/car_ui_list_item_touch_interceptor"
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:background="@drawable/car_ui_list_item_background"
@@ -38,14 +38,14 @@
 
     <!-- This touch interceptor does not include the action container -->
     <View
-        android:id="@+id/reduced_touch_interceptor"
+        android:id="@+id/car_ui_list_item_reduced_touch_interceptor"
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:background="@drawable/car_ui_list_item_background"
         android:clickable="true"
         android:visibility="gone"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/action_container"
+        app:layout_constraintEnd_toStartOf="@id/car_ui_list_item_action_container"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
@@ -57,7 +57,7 @@
         app:layout_constraintGuide_begin="@dimen/car_ui_list_item_start_inset" />
 
     <FrameLayout
-        android:id="@+id/icon_container"
+        android:id="@+id/car_ui_list_item_icon_container"
         android:layout_width="@dimen/car_ui_list_item_icon_container_width"
         android:layout_height="0dp"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -65,7 +65,7 @@
         app:layout_constraintTop_toTopOf="parent">
 
         <ImageView
-            android:id="@+id/icon"
+            android:id="@+id/car_ui_list_item_icon"
             android:layout_width="@dimen/car_ui_list_item_icon_size"
             android:layout_height="@dimen/car_ui_list_item_icon_size"
             android:layout_gravity="center"
@@ -73,7 +73,7 @@
             android:scaleType="fitXY" />
 
         <ImageView
-            android:id="@+id/content_icon"
+            android:id="@+id/car_ui_list_item_content_icon"
             android:layout_width="@dimen/car_ui_list_item_content_icon_width"
             android:layout_height="@dimen/car_ui_list_item_content_icon_height"
             android:layout_gravity="center"
@@ -81,7 +81,7 @@
             android:scaleType="fitXY" />
 
         <ImageView
-            android:id="@+id/avatar_icon"
+            android:id="@+id/car_ui_list_item_avatar_icon"
             android:background="@drawable/car_ui_list_item_avatar_icon_outline"
             android:layout_width="@dimen/car_ui_list_item_avatar_icon_width"
             android:layout_height="@dimen/car_ui_list_item_avatar_icon_height"
@@ -93,19 +93,19 @@
     <!-- Use nested layout as a workaround for a regression in constraintlayout where chains not
     are not rendered correctly when the tail is gone (b/168627311).-->
     <LinearLayout
-        android:id="@+id/text_container"
+        android:id="@+id/car_ui_list_item_text_container"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/action_container"
-        app:layout_constraintStart_toEndOf="@id/icon_container"
+        app:layout_constraintEnd_toStartOf="@id/car_ui_list_item_action_container"
+        app:layout_constraintStart_toEndOf="@id/car_ui_list_item_icon_container"
         android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin"
         app:layout_goneMarginStart="@dimen/car_ui_list_item_text_no_icon_start_margin"
         android:orientation="vertical">
 
         <TextView
-            android:id="@+id/title"
+            android:id="@+id/car_ui_list_item_title"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:singleLine="@bool/car_ui_list_item_single_line_title"
@@ -113,7 +113,7 @@
             android:textAppearance="@style/TextAppearance.CarUi.ListItem" />
 
         <TextView
-            android:id="@+id/body"
+            android:id="@+id/car_ui_list_item_body"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:textDirection="locale"
@@ -122,19 +122,19 @@
 
     <!-- This touch interceptor is sized and positioned to encompass the action container   -->
     <View
-        android:id="@+id/action_container_touch_interceptor"
+        android:id="@+id/car_ui_list_item_action_container_touch_interceptor"
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:background="@drawable/car_ui_list_item_background"
         android:clickable="true"
         android:visibility="gone"
-        app:layout_constraintBottom_toBottomOf="@id/action_container"
-        app:layout_constraintEnd_toEndOf="@id/action_container"
-        app:layout_constraintStart_toStartOf="@id/action_container"
-        app:layout_constraintTop_toTopOf="@id/action_container" />
+        app:layout_constraintBottom_toBottomOf="@id/car_ui_list_item_action_container"
+        app:layout_constraintEnd_toEndOf="@id/car_ui_list_item_action_container"
+        app:layout_constraintStart_toStartOf="@id/car_ui_list_item_action_container"
+        app:layout_constraintTop_toTopOf="@id/car_ui_list_item_action_container" />
 
     <FrameLayout
-        android:id="@+id/action_container"
+        android:id="@+id/car_ui_list_item_action_container"
         android:layout_width="wrap_content"
         android:minWidth="@dimen/car_ui_list_item_icon_container_width"
         android:layout_height="0dp"
@@ -143,14 +143,14 @@
         app:layout_constraintTop_toTopOf="parent">
 
         <View
-            android:id="@+id/action_divider"
+            android:id="@+id/car_ui_list_item_action_divider"
             android:layout_width="@dimen/car_ui_list_item_action_divider_width"
             android:layout_height="@dimen/car_ui_list_item_action_divider_height"
             android:layout_gravity="start|center_vertical"
             android:background="@drawable/car_ui_list_item_divider" />
 
         <Switch
-            android:id="@+id/switch_widget"
+            android:id="@+id/car_ui_list_item_switch_widget"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
@@ -158,7 +158,7 @@
             android:focusable="false" />
 
         <CheckBox
-            android:id="@+id/checkbox_widget"
+            android:id="@+id/car_ui_list_item_checkbox_widget"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
@@ -166,7 +166,7 @@
             android:focusable="false" />
 
         <RadioButton
-            android:id="@+id/radio_button_widget"
+            android:id="@+id/car_ui_list_item_radio_button_widget"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
@@ -174,7 +174,7 @@
             android:focusable="false" />
 
         <ImageView
-            android:id="@+id/supplemental_icon"
+            android:id="@+id/car_ui_list_item_supplemental_icon"
             android:layout_width="@dimen/car_ui_list_item_supplemental_icon_size"
             android:layout_height="@dimen/car_ui_list_item_supplemental_icon_size"
             android:layout_gravity="center"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference.xml
index 52dbd68..ef60b83 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference.xml
@@ -15,7 +15,7 @@
     limitations under the License.
 -->
 
-<RelativeLayout
+<com.android.car.ui.uxr.DrawableStateRelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -26,7 +26,7 @@
     android:tag="carUiPreference"
     android:paddingStart="?android:attr/listPreferredItemPaddingStart">
 
-    <ImageView
+    <com.android.car.ui.uxr.DrawableStateImageView
         android:id="@android:id/icon"
         android:layout_width="@dimen/car_ui_preference_icon_size"
         android:layout_height="@dimen/car_ui_preference_icon_size"
@@ -48,14 +48,14 @@
         android:layout_toStartOf="@android:id/widget_frame"
         android:orientation="vertical">
 
-        <TextView
+        <com.android.car.ui.uxr.DrawableStateTextView
             android:id="@android:id/title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:singleLine="true"
             android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"/>
 
-        <TextView
+        <com.android.car.ui.uxr.DrawableStateTextView
             android:id="@android:id/summary"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
@@ -71,4 +71,4 @@
         android:layout_alignParentEnd="true"
         android:layout_centerVertical="true"/>
 
-</RelativeLayout>
+</com.android.car.ui.uxr.DrawableStateRelativeLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_icon.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_icon.xml
new file mode 100644
index 0000000..0d0f202
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_icon.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2019 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:tag="carUiPreference">
+
+    <com.android.car.ui.uxr.DrawableStateConstraintLayout
+        android:id="@+id/car_ui_first_action_container"
+        android:layout_height="0dp"
+        android:layout_width="0dp"
+        android:background="?android:attr/selectableItemBackground"
+        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/car_ui_second_action_container"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+        <com.android.car.ui.uxr.DrawableStateImageView
+            style="@style/Preference.CarUi.Icon"
+            android:id="@android:id/icon"
+            android:layout_width="@dimen/car_ui_preference_icon_size"
+            android:layout_height="@dimen/car_ui_preference_icon_size"
+            android:scaleType="fitCenter"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+            app:layout_goneMarginStart="0dp"
+            android:textDirection="locale"
+            android:singleLine="true"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"
+            app:layout_constraintStart_toEndOf="@android:id/icon"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@android:id/summary"
+            app:layout_constraintVertical_chainStyle="packed"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/summary"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+            app:layout_goneMarginStart="0dp"
+            android:textDirection="locale"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"
+            android:maxLines="2"
+            app:layout_constraintStart_toEndOf="@android:id/icon"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@android:id/title"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+    </com.android.car.ui.uxr.DrawableStateConstraintLayout>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/car_ui_second_action_container"
+        android:layout_height="0dp"
+        android:layout_width="wrap_content"
+        app:layout_constraintStart_toEndOf="@id/car_ui_first_action_container"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+
+        <View
+            android:id="@+id/car_ui_divider"
+            android:layout_width="@dimen/car_ui_divider_width"
+            android:layout_height="0dp"
+            android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+            android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/car_ui_secondary_action"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            style="@style/Preference.CarUi.Divider"/>
+
+        <com.android.car.ui.uxr.DrawableStateFrameLayout
+            android:id="@+id/car_ui_secondary_action"
+            android:layout_width="?android:attr/listPreferredItemHeightSmall"
+            android:layout_height="match_parent"
+            android:background="?android:attr/selectableItemBackground"
+            android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+            android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+            app:layout_constraintStart_toEndOf="@id/car_ui_divider"
+            app:layout_constraintEnd_toStartOf="@android:id/widget_frame"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent">
+            <com.android.car.ui.uxr.DrawableStateImageView
+                android:id="@+id/car_ui_secondary_action_concrete"
+                android:layout_width="48dp"
+                android:layout_height="48dp"
+                android:layout_gravity="center"
+                android:tintMode="src_in"
+                android:tint="@color/car_ui_text_color_primary"/>
+        </com.android.car.ui.uxr.DrawableStateFrameLayout>
+
+        <!-- The widget frame is required for androidx preferences, but we won't use it. -->
+        <FrameLayout
+            android:id="@android:id/widget_frame"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_switch.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_switch.xml
new file mode 100644
index 0000000..e89b844
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_switch.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2019 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:tag="carUiPreference">
+
+    <com.android.car.ui.uxr.DrawableStateConstraintLayout
+        android:id="@+id/car_ui_first_action_container"
+        android:layout_height="0dp"
+        android:layout_width="0dp"
+        android:background="?android:attr/selectableItemBackground"
+        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/car_ui_second_action_container"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+        <com.android.car.ui.uxr.DrawableStateImageView
+            style="@style/Preference.CarUi.Icon"
+            android:id="@android:id/icon"
+            android:layout_width="@dimen/car_ui_preference_icon_size"
+            android:layout_height="@dimen/car_ui_preference_icon_size"
+            android:scaleType="fitCenter"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+            app:layout_goneMarginStart="0dp"
+            android:textDirection="locale"
+            android:singleLine="true"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"
+            app:layout_constraintStart_toEndOf="@android:id/icon"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@android:id/summary"
+            app:layout_constraintVertical_chainStyle="packed"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/summary"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+            app:layout_goneMarginStart="0dp"
+            android:textDirection="locale"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"
+            android:maxLines="2"
+            app:layout_constraintStart_toEndOf="@android:id/icon"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@android:id/title"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+    </com.android.car.ui.uxr.DrawableStateConstraintLayout>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/car_ui_second_action_container"
+        android:layout_height="0dp"
+        android:layout_width="wrap_content"
+        app:layout_constraintStart_toEndOf="@id/car_ui_first_action_container"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+
+        <View
+            android:id="@+id/car_ui_divider"
+            android:layout_width="@dimen/car_ui_divider_width"
+            android:layout_height="0dp"
+            android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+            android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/car_ui_secondary_action"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            style="@style/Preference.CarUi.Divider"/>
+
+        <com.android.car.ui.uxr.DrawableStateFrameLayout
+            android:id="@+id/car_ui_secondary_action"
+            android:layout_width="?android:attr/listPreferredItemHeightSmall"
+            android:layout_height="match_parent"
+            android:background="?android:attr/selectableItemBackground"
+            android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+            android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+            app:layout_constraintStart_toEndOf="@id/car_ui_divider"
+            app:layout_constraintEnd_toStartOf="@android:id/widget_frame"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent">
+            <com.android.car.ui.uxr.DrawableStateSwitch
+                android:id="@+id/car_ui_secondary_action_concrete"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:clickable="false"
+                android:focusable="false"/>
+        </com.android.car.ui.uxr.DrawableStateFrameLayout>
+
+        <!-- The widget frame is required for androidx preferences, but we won't use it. -->
+        <FrameLayout
+            android:id="@android:id/widget_frame"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text.xml
new file mode 100644
index 0000000..bfdcbba
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2019 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:tag="carUiPreference">
+
+    <com.android.car.ui.uxr.DrawableStateConstraintLayout
+        android:id="@+id/car_ui_first_action_container"
+        android:layout_height="0dp"
+        android:layout_width="0dp"
+        android:background="?android:attr/selectableItemBackground"
+        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/car_ui_second_action_container"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+        <com.android.car.ui.uxr.DrawableStateImageView
+            style="@style/Preference.CarUi.Icon"
+            android:id="@android:id/icon"
+            android:layout_width="@dimen/car_ui_preference_icon_size"
+            android:layout_height="@dimen/car_ui_preference_icon_size"
+            android:scaleType="fitCenter"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+            app:layout_goneMarginStart="0dp"
+            android:textDirection="locale"
+            android:singleLine="true"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"
+            app:layout_constraintStart_toEndOf="@android:id/icon"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@android:id/summary"
+            app:layout_constraintVertical_chainStyle="packed"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/summary"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+            app:layout_goneMarginStart="0dp"
+            android:textDirection="locale"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"
+            android:maxLines="2"
+            app:layout_constraintStart_toEndOf="@android:id/icon"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@android:id/title"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+    </com.android.car.ui.uxr.DrawableStateConstraintLayout>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/car_ui_second_action_container"
+        android:layout_height="0dp"
+        android:layout_width="wrap_content"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+        app:layout_constraintStart_toEndOf="@id/car_ui_first_action_container"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+
+        <View
+            android:id="@+id/car_ui_divider"
+            android:layout_width="@dimen/car_ui_divider_width"
+            android:layout_height="0dp"
+            android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+            android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+            android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/car_ui_secondary_action"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            style="@style/Preference.CarUi.Divider"/>
+
+        <com.android.car.ui.uxr.DrawableStateButton
+            android:id="@+id/car_ui_secondary_action"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintStart_toEndOf="@id/car_ui_divider"
+            app:layout_constraintEnd_toStartOf="@android:id/widget_frame"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+
+        <!-- The widget frame is required for androidx preferences, but we won't use it. -->
+        <FrameLayout
+            android:id="@android:id/widget_frame"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text_borderless.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text_borderless.xml
new file mode 100644
index 0000000..4553739
--- /dev/null
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_preference_two_action_text_borderless.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 2019 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:tag="carUiPreference">
+
+    <com.android.car.ui.uxr.DrawableStateConstraintLayout
+        android:id="@+id/car_ui_first_action_container"
+        android:layout_height="0dp"
+        android:layout_width="0dp"
+        android:background="?android:attr/selectableItemBackground"
+        android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/car_ui_second_action_container"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+        <com.android.car.ui.uxr.DrawableStateImageView
+            style="@style/Preference.CarUi.Icon"
+            android:id="@android:id/icon"
+            android:layout_width="@dimen/car_ui_preference_icon_size"
+            android:layout_height="@dimen/car_ui_preference_icon_size"
+            android:scaleType="fitCenter"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+            app:layout_goneMarginStart="0dp"
+            android:textDirection="locale"
+            android:singleLine="true"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"
+            app:layout_constraintStart_toEndOf="@android:id/icon"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@android:id/summary"
+            app:layout_constraintVertical_chainStyle="packed"/>
+
+        <com.android.car.ui.uxr.DrawableStateTextView
+            android:id="@android:id/summary"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/car_ui_preference_icon_margin_end"
+            app:layout_goneMarginStart="0dp"
+            android:textDirection="locale"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"
+            android:maxLines="2"
+            app:layout_constraintStart_toEndOf="@android:id/icon"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@android:id/title"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+    </com.android.car.ui.uxr.DrawableStateConstraintLayout>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/car_ui_second_action_container"
+        android:layout_height="0dp"
+        android:layout_width="wrap_content"
+        android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+        app:layout_constraintStart_toEndOf="@id/car_ui_first_action_container"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent">
+
+        <View
+            android:id="@+id/car_ui_divider"
+            android:layout_width="@dimen/car_ui_divider_width"
+            android:layout_height="0dp"
+            android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+            android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+            android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/car_ui_secondary_action"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            style="@style/Preference.CarUi.Divider"/>
+
+        <com.android.car.ui.uxr.DrawableStateButton
+            android:id="@+id/car_ui_secondary_action"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            style="?android:attr/borderlessButtonStyle"
+            app:layout_constraintStart_toEndOf="@id/car_ui_divider"
+            app:layout_constraintEnd_toStartOf="@android:id/widget_frame"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+
+        <!-- The widget frame is required for androidx preferences, but we won't use it. -->
+        <FrameLayout
+            android:id="@android:id/widget_frame"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"/>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml
index a0c955f..6e12515 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recycler_view.xml
@@ -16,13 +16,18 @@
   -->
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <include layout="@layout/car_ui_recyclerview_scrollbar"/>
-
     <com.android.car.ui.recyclerview.CarUiRecyclerViewContainer
         android:id="@+id/car_ui_recycler_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginEnd="@dimen/car_ui_scrollbar_margin"
-        android:tag="carUiRecyclerView"
-        android:layout_weight="1"/>
+        android:paddingStart="@dimen/car_ui_scrollbar_margin"
+        android:paddingEnd="@dimen/car_ui_scrollbar_margin"
+        android:tag="carUiRecyclerView" />
+
+    <FrameLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="left" >
+        <include layout="@layout/car_ui_recyclerview_scrollbar"/>
+    </FrameLayout>
 </merge>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recyclerview_scrollbar.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recyclerview_scrollbar.xml
index 5896819..d5e626a 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recyclerview_scrollbar.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_recyclerview_scrollbar.xml
@@ -45,7 +45,8 @@
         android:layout_height="0dp"
         android:layout_gravity="center_horizontal"
         android:background="@drawable/car_ui_recyclerview_scrollbar_thumb"
-        app:layout_constraintTop_toBottomOf="@+id/car_ui_scrollbar_page_up"
+        app:layout_constraintTop_toTopOf="@+id/car_ui_scrollbar_track"
+        app:layout_constraintBottom_toBottomOf="@+id/car_ui_scrollbar_track"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"/>
 
diff --git a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_two_action_preference.xml b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_two_action_preference.xml
index 7c633e0..3f3ffb0 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_two_action_preference.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/layout/car_ui_two_action_preference.xml
@@ -22,7 +22,7 @@
     android:background="@android:color/transparent"
     android:gravity="center_vertical"
     android:minHeight="?android:attr/listPreferredItemHeightSmall">
-    <LinearLayout
+    <com.android.car.ui.uxr.DrawableStateLinearLayout
         android:id="@+id/car_ui_preference_container_without_widget"
         android:layout_width="0dp"
         android:layout_height="match_parent"
@@ -44,20 +44,20 @@
             android:layout_height="wrap_content"
             android:layout_centerVertical="true"
             android:orientation="vertical">
-            <TextView
+            <com.android.car.ui.uxr.DrawableStateTextView
                 android:id="@android:id/title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:ellipsize="end"
                 android:singleLine="true"
                 android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"/>
-            <TextView
+            <com.android.car.ui.uxr.DrawableStateTextView
                 android:id="@android:id/summary"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"/>
         </LinearLayout>
-    </LinearLayout>
+    </com.android.car.ui.uxr.DrawableStateLinearLayout>
     <LinearLayout
         android:id="@+id/action_widget_container"
         android:layout_width="wrap_content"
@@ -69,7 +69,7 @@
             android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
             style="@style/Preference.CarUi.Divider"/>
         <!-- Preference should place its actual preference widget here. -->
-        <FrameLayout
+        <com.android.car.ui.uxr.DrawableStateFrameLayout
             android:id="@android:id/widget_frame"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml b/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
index 0ca5abe..2a5bd75 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/attrs.xml
@@ -130,18 +130,43 @@
             <!-- grid layout -->
             <enum name="grid" value="1" />
         </attr>
+
+        <!-- car ui recyclerview orientation -->
+        <attr name="android:orientation" />
     </declare-styleable>
 
     <declare-styleable name="CarUiPreference">
         <!-- Toggle for showing chevron -->
         <attr name="showChevron" format="boolean" />
-        <!-- Show ripple when disabled preference is clicked -->
-        <attr name="showRippleOnDisabledPreference" format="boolean" />
+        <!-- Display this preference as ux restricted. -->
+        <attr name="car_ui_ux_restricted" format="boolean" />
     </declare-styleable>
 
     <declare-styleable name="CarUiTwoActionPreference">
         <!-- Determines if the secondary action is initially shown -->
         <attr name="actionShown" format="boolean"/>
+        <!-- Determines if the secondary action is initially enabled -->
+        <attr name="actionEnabled" format="boolean"/>
+    </declare-styleable>
+
+    <declare-styleable name="CarUiTwoActionBasePreference">
+        <!-- All of these are disallowed -->
+        <attr name="layout" format="reference"/>
+        <attr name="android:layout" format="reference"/>
+        <attr name="widgetLayout" format="reference"/>
+        <attr name="android:widgetLayout" format="reference"/>
+    </declare-styleable>
+
+    <declare-styleable name="CarUiTwoActionTextPreference">
+        <attr name="secondaryActionStyle" format="enum">
+            <enum name="bordered" value="0"/>
+            <enum name="borderless" value="1"/>
+        </attr>
+        <attr name="secondaryActionText" format="string"/>
+    </declare-styleable>
+
+    <declare-styleable name="CarUiTwoActionIconPreference">
+        <attr name="secondaryActionIcon" format="reference"/>
     </declare-styleable>
 
     <!-- Theme attribute to specify a default style for all CarUiPreferences -->
@@ -161,25 +186,30 @@
                  view. The target view is chosen in the following order:
                    1. the "android:focusedByDefault" view, if any
                    2. the "app:defaultFocus" view, if any
-                   3. the first focusable item in a scrollable container, if any
-                   4. previously focused view, if any and the cache is not stale
-                   5. the first focusable view, if any
-                 Note that 4 will be prioritized over 1&2&3 when
-                 car_ui_focus_area_default_focus_overrides_history is false.
+                   3. the selected item in a scrollable container, if any
+                   4. the first focusable item in a scrollable container, if any
+                   5. previously focused view, if any and the cache is not stale
+                   6. the first focusable view, if any
+                 Note that 5 will be prioritized over 1, 2, 3, and 4 when
+                 "app:defaultFocusOverridesHistory" is true.
              (2) When it needs to initialize the focus (such as when a window is opened), it will
                  search for a view in the window and focus on it. The view is chosen in the
                  following order:
                    1. the first "android:focusedByDefault" view, if any
                    2. the first "app:defaultFocus" view, if any
-                   3. the first focusable item in a scrollable container, if any
-                   4. the first focusable view that is not a FocusParkingView, if any
+                   3. the selected item in a scrollable container, if any
+                   4. the first focusable item in a scrollable container, if any
+                   5. the first focusable view that is not a FocusParkingView, if any
              If there is only one FocusArea that needs to set default focus, you can use either
              "app:defaultFocus" or "android:focusedByDefault". If there are more than one, you
              should use "android:focusedByDefault" in the primary FocusArea, and use
              "app:defaultFocus" in other FocusAreas. -->
-
         <attr name="defaultFocus" format="reference"/>
 
+        <!-- Whether to focus on the default focus view when nudging to the FocusArea, even if there
+             was another view in the FocusArea focused before. -->
+        <attr name="defaultFocusOverridesHistory" format="boolean"/>
+
         <!-- The paddings of FocusArea highlight. It does't impact the paddings on its child views,
              or vice versa. -->
         <!-- The start padding of the FocusArea highlight. -->
@@ -242,6 +272,11 @@
         <attr name="nudgeUp" format="reference"/>
         <!-- The ID of the target FocusArea when nudging down. -->
         <attr name="nudgeDown" format="reference"/>
+
+        <!-- Whether rotation wraps around. When true, rotation wraps around, staying within the
+             FocusArea, when it reaches the first or last focusable view in the FocusArea. When
+             false, rotation does nothing in this case. -->
+        <attr name="wrapAround" format="boolean"/>
     </declare-styleable>
 
     <!-- Attributes for FocusParkingView. -->
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml b/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml
index ccf9c4d..ab575b2 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/bools.xml
@@ -44,6 +44,8 @@
     <bool name="car_ui_preference_show_chevron">false</bool>
     <!-- whether list preference should be shown in full screen or as a dialog -->
     <bool name="car_ui_preference_list_show_full_screen">true</bool>
+    <!-- Whether list and multi-select preference selection changes should propagate instantly -->
+    <bool name="car_ui_preference_list_instant_change_callback">false</bool>
 
     <!-- List items -->
 
@@ -57,9 +59,8 @@
     <!-- Whether to draw highlight (car_ui_focus_area_background_highlight) on top of the FocusArea
          but behind its children when the FocusArea's descendant gets focused. -->
     <bool name="car_ui_enable_focus_area_background_highlight">false</bool>
-    <!-- Whether to focus on the app:defaultFocus view when nudging to the FocusArea, even if there
-         was another view focused in the FocusArea. -->
-    <bool name="car_ui_focus_area_default_focus_overrides_history">true</bool>
+    <!-- Deprecated. -->
+    <bool name="car_ui_focus_area_default_focus_overrides_history">false</bool>
     <!-- Whether to clear FocusArea history when the user rotates the rotary controller. -->
     <bool name="car_ui_clear_focus_area_history_when_rotating">true</bool>
 
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml b/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml
index d355f99..998d58f 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/dimens.xml
@@ -247,4 +247,10 @@
     <dimen name="car_ui_ime_wide_search_item_sub_title_padding_top">22dp</dimen>
     <dimen name="car_ui_ime_wide_search_item_sub_title_text_size">24dp</dimen>
     <dimen name="car_ui_ime_wide_search_item_secondary_image_padding_left">36dp</dimen>
+
+    <!-- Padding towards the end of the input field. We have an overlay on top
+     of input text field that displays the clear and the error icon. This
+     padding is needed so that the icon does not overlap the input field. The
+     padding is calculated by 24dp + car_ui_primary_icon_size (44dp). -->
+    <dimen name="car_ui_ime_wide_screen_input_area_padding_end">68dp</dimen>
 </resources>
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/styles.xml b/car-ui-lib/car-ui-lib/src/main/res/values/styles.xml
index 00cb56e..bd033ec 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/styles.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/styles.xml
@@ -246,7 +246,7 @@
     <!-- TextAppearance -->
 
     <style name="TextAppearance.CarUi" parent="android:TextAppearance.DeviceDefault">
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textColor">@color/car_ui_text_color_primary</item>
         <item name="android:textAlignment">viewStart</item>
     </style>
 
diff --git a/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml b/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml
index a599fa0..c2069b1 100644
--- a/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml
+++ b/car-ui-lib/car-ui-lib/src/main/res/values/themes.xml
@@ -35,6 +35,7 @@
 
         <item name="selectableItemBackground">?android:attr/selectableItemBackground</item>
         <item name="selectableItemBackgroundBorderless">?android:attr/selectableItemBackgroundBorderless</item>
+        <item name="android:borderlessButtonStyle">@style/Widget.CarUi.Button.Borderless.Colored</item>
         <item name="borderlessButtonStyle">?android:attr/borderlessButtonStyle</item>
         <item name="homeAsUpIndicator">?android:attr/homeAsUpIndicator</item>
 
@@ -208,6 +209,8 @@
 
         <!-- textAppearance -->
         <item name="android:textAppearance">@style/TextAppearance.CarUi</item>
+        <!--@color/transparent does not completely remove highlight  -->
+        <item name="android:textColorHighlight">#00FFFFFF</item>
     </style>
 
     <!-- TODO(b/150230923) remove this when other apps are ready -->
diff --git a/car-ui-lib/car-ui-lib/src/test/java/com/android/car/ui/recyclerview/CarUiListItemTest.java b/car-ui-lib/car-ui-lib/src/test/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
index cf28632..c54dab4 100644
--- a/car-ui-lib/car-ui-lib/src/test/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
+++ b/car-ui-lib/car-ui-lib/src/test/java/com/android/car/ui/recyclerview/CarUiListItemTest.java
@@ -62,33 +62,38 @@
     }
 
     private View getListItemTitleAtPosition(int position) {
-        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.title);
+        return getListItemViewHolderAtPosition(position).itemView
+                .findViewById(R.id.car_ui_list_item_title);
     }
 
     private View getListItemBodyAtPosition(int position) {
-        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.body);
+        return getListItemViewHolderAtPosition(position).itemView
+                .findViewById(R.id.car_ui_list_item_body);
     }
 
     private View getListItemIconContainerAtPosition(int position) {
-        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.icon_container);
+        return getListItemViewHolderAtPosition(position).itemView
+                .findViewById(R.id.car_ui_list_item_icon_container);
     }
 
     private View getListItemActionContainerAtPosition(int position) {
-        return getListItemViewHolderAtPosition(position)
-                .itemView.findViewById(R.id.action_container);
+        return getListItemViewHolderAtPosition(position).itemView
+                .findViewById(R.id.action_container);
     }
 
     private Switch getListItemSwitchAtPosition(int position) {
-        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.switch_widget);
+        return getListItemViewHolderAtPosition(position).itemView
+                .findViewById(R.id.car_ui_list_item_switch_widget);
     }
 
     private CheckBox getListItemCheckBoxAtPosition(int position) {
-        return getListItemViewHolderAtPosition(position)
-                .itemView.findViewById(R.id.checkbox_widget);
+        return getListItemViewHolderAtPosition(position).itemView
+                .findViewById(R.id.car_ui_list_item_checkbox_widget);
     }
 
     private View getListItemIconAtPosition(int position) {
-        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.icon);
+        return getListItemViewHolderAtPosition(position).itemView
+                .findViewById(R.id.icon);
     }
 
     private CarUiListItemAdapter.HeaderViewHolder getHeaderViewHolderAtPosition(int position) {
@@ -97,11 +102,13 @@
     }
 
     private TextView getHeaderViewHolderTitleAtPosition(int position) {
-        return getHeaderViewHolderAtPosition(position).itemView.findViewById(R.id.title);
+        return getHeaderViewHolderAtPosition(position).itemView
+                .findViewById(R.id.car_ui_list_item_title);
     }
 
     private TextView getHeaderViewHolderBodyAtPosition(int position) {
-        return getHeaderViewHolderAtPosition(position).itemView.findViewById(R.id.body);
+        return getHeaderViewHolderAtPosition(position).itemView
+                .findViewById(R.id.car_ui_list_item_body);
     }
 
     private void updateRecyclerViewAdapter(CarUiListItemAdapter adapter) {
diff --git a/car-ui-lib/paintbooth/AndroidManifest.xml b/car-ui-lib/paintbooth/AndroidManifest.xml
index 9763b47..dd2e3e7 100644
--- a/car-ui-lib/paintbooth/AndroidManifest.xml
+++ b/car-ui-lib/paintbooth/AndroidManifest.xml
@@ -53,6 +53,7 @@
         <action android:name="android.intent.action.MAIN"/>
         <category android:name="android.intent.category.LAUNCHER"/>
       </intent-filter>
+      <meta-data android:name="distractionOptimized" android:value="true"/>
     </activity>
 
     <activity
@@ -70,6 +71,12 @@
     <activity
         android:name=".preferences.PreferenceActivity"
         android:exported="false"
+        android:parentActivityName=".MainActivity">
+      <meta-data android:name="distractionOptimized" android:value="true"/>
+    </activity>
+    <activity
+        android:name=".preferences.SplitPreferenceActivity"
+        android:exported="false"
         android:parentActivityName=".MainActivity"/>
     <activity
         android:name=".widescreenime.WideScreenImeActivity"
diff --git a/car-ui-lib/paintbooth/build.gradle b/car-ui-lib/paintbooth/build.gradle
index 94ab49e..739e592 100644
--- a/car-ui-lib/paintbooth/build.gradle
+++ b/car-ui-lib/paintbooth/build.gradle
@@ -42,6 +42,9 @@
             }
         }
     }
+
+    // This is the gradle equivalent of the libs: ["android.car"] in the Android.bp
+    useLibrary 'android.car'
 }
 
 dependencies {
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java
index e9f990d..b7636ad 100644
--- a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java
@@ -16,33 +16,111 @@
 
 package com.android.car.ui.paintbooth.preferences;
 
+import android.car.drivingstate.CarUxRestrictions;
 import android.os.Bundle;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
 
 import com.android.car.ui.paintbooth.R;
-import com.android.car.ui.preference.CarUiPreference;
+import com.android.car.ui.preference.CarUiTwoActionBasePreference;
+import com.android.car.ui.preference.CarUiTwoActionIconPreference;
+import com.android.car.ui.preference.CarUiTwoActionSwitchPreference;
+import com.android.car.ui.preference.CarUiTwoActionTextPreference;
 import com.android.car.ui.preference.PreferenceFragment;
+import com.android.car.ui.preference.UxRestrictablePreference;
+import com.android.car.ui.utils.CarUxRestrictionsUtil;
+
+import java.util.Objects;
 
 /**
  * Fragment to load preferences
  */
 public class PreferenceDemoFragment extends PreferenceFragment {
 
+    CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mOnUxRestrictionsChangedListener =
+            restrictions -> {
+                boolean isRestricted =
+                        (restrictions.getActiveRestrictions()
+                                & CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP)
+                                == CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP;
+
+                restrictPreference(getPreferenceScreen(), isRestricted);
+            };
+
+    private void restrictPreference(Preference preference, boolean restrict) {
+        if (preference == null) {
+            return;
+        }
+
+        if (preference instanceof UxRestrictablePreference) {
+            ((UxRestrictablePreference) preference).setUxRestricted(restrict);
+            ((UxRestrictablePreference) preference).setOnClickWhileRestrictedListener(p ->
+                    Toast.makeText(getContext(), R.string.car_ui_restricted_while_driving,
+                            Toast.LENGTH_LONG).show());
+        }
+
+        if (preference instanceof PreferenceGroup) {
+            PreferenceGroup preferenceGroup = (PreferenceGroup) preference;
+            for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
+                restrictPreference(preferenceGroup.getPreference(i), restrict);
+            }
+        }
+    }
+
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
         // Load the preferences from an XML resource
         setPreferencesFromResource(R.xml.preference_samples, rootKey);
-        CarUiPreference preferenceDisabledWithoutRipple = findPreference(
-                "preference_disabled_without_ripple");
-        preferenceDisabledWithoutRipple.setEnabled(false);
-        preferenceDisabledWithoutRipple.setMessageToShowWhenDisabledPreferenceClicked(
-                "I am disabled because...");
-        preferenceDisabledWithoutRipple.setShouldShowRippleOnDisabledPreference(false);
 
-        CarUiPreference preferenceDisabledWithRipple = findPreference(
-                "preference_disabled_with_ripple");
-        preferenceDisabledWithRipple.setEnabled(false);
-        preferenceDisabledWithRipple.setMessageToShowWhenDisabledPreferenceClicked(
-                "I am disabled because...");
-        preferenceDisabledWithRipple.setShouldShowRippleOnDisabledPreference(true);
+        setupTwoActionPreferenceClickListeners(requirePreference("twoactiontext"));
+        setupTwoActionPreferenceClickListeners(requirePreference("twoactiontextborderless"));
+        setupTwoActionPreferenceClickListeners(requirePreference("twoactionicon"));
+        setupTwoActionPreferenceClickListeners(requirePreference("twoactionswitch"));
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        CarUxRestrictionsUtil.getInstance(requireContext())
+                .register(mOnUxRestrictionsChangedListener);
+    }
+
+    @Override
+    public void onStop() {
+        CarUxRestrictionsUtil.getInstance(requireContext())
+                .unregister(mOnUxRestrictionsChangedListener);
+        super.onStop();
+    }
+
+    private void setupTwoActionPreferenceClickListeners(CarUiTwoActionBasePreference preference) {
+        if (preference instanceof CarUiTwoActionSwitchPreference) {
+            ((CarUiTwoActionSwitchPreference) preference).setOnSecondaryActionClickListener(
+                    (selected) -> preference.setSecondaryActionEnabled(false));
+        } else if (preference instanceof CarUiTwoActionTextPreference) {
+            ((CarUiTwoActionTextPreference) preference).setOnSecondaryActionClickListener(
+                    () -> preference.setSecondaryActionEnabled(false));
+        } else {
+            ((CarUiTwoActionIconPreference) preference).setOnSecondaryActionClickListener(
+                    () -> preference.setSecondaryActionEnabled(false));
+        }
+
+        preference.setOnPreferenceClickListener((pref) -> {
+            if (!preference.isSecondaryActionEnabled()) {
+                preference.setSecondaryActionEnabled(true);
+            } else {
+                preference.setSecondaryActionVisible(
+                        !preference.isSecondaryActionVisible());
+            }
+            return true;
+        });
+    }
+
+    @NonNull
+    private <T extends Preference> T requirePreference(CharSequence key) {
+        T pref = findPreference(key);
+        return Objects.requireNonNull(pref);
     }
 }
diff --git a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java
index 71eb954..2eb3063 100644
--- a/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java
+++ b/car-ui-lib/paintbooth/src/main/java/com/android/car/ui/paintbooth/widescreenime/WideScreenImeActivity.java
@@ -101,7 +101,7 @@
                 });
 
         CarUiContentListItem.OnClickListener mainClickListener = i ->
-                Toast.makeText(this, "Item clicked!", Toast.LENGTH_SHORT).show();
+                Toast.makeText(this, "Item clicked! " + i.getTitle(), Toast.LENGTH_SHORT).show();
         CarUiContentListItem.OnClickListener secondaryClickListener = i ->
                 Toast.makeText(this, "Item's secondary action clicked!", Toast.LENGTH_SHORT).show();
 
diff --git a/car-ui-lib/paintbooth/src/main/res/drawable/ic_settings_wifi.xml b/car-ui-lib/paintbooth/src/main/res/drawable/ic_settings_wifi.xml
index 9a09d70..c16424c 100644
--- a/car-ui-lib/paintbooth/src/main/res/drawable/ic_settings_wifi.xml
+++ b/car-ui-lib/paintbooth/src/main/res/drawable/ic_settings_wifi.xml
@@ -16,12 +16,9 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="64dp"
     android:height="64dp"
-    android:viewportWidth="64"
-    android:viewportHeight="64">
+    android:viewportWidth="24"
+    android:viewportHeight="24">
   <path
-      android:pathData="M36.87,39.05a7,7 0,1 1,-9.901 9.9,7 7,0 0,1 9.901,-9.9zM14.243,26.323c9.763,-9.764 25.593,-9.764 35.355,0M53.84,22.08C41.735,9.972 22.106,9.972 10,22.08M18.486,30.565c7.42,-7.42 19.449,-7.42 26.869,0M22.728,34.808c5.077,-5.077 13.308,-5.077 18.385,0"
-      android:strokeWidth="2"
-      android:fillColor="#00000000"
-      android:fillType="evenOdd"
-      android:strokeColor="#FFF"/>
+      android:pathData="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"
+      android:fillColor="@color/car_ui_text_color_primary"/>
 </vector>
diff --git a/connected-device-lib/res/values/config.xml b/car-ui-lib/paintbooth/src/main/res/values/overlayable.xml
similarity index 62%
rename from connected-device-lib/res/values/config.xml
rename to car-ui-lib/paintbooth/src/main/res/values/overlayable.xml
index c719462..0577d92 100644
--- a/connected-device-lib/res/values/config.xml
+++ b/car-ui-lib/paintbooth/src/main/res/values/overlayable.xml
@@ -1,6 +1,6 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version='1.0' encoding='UTF-8'?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ 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.
@@ -14,7 +14,11 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="connected_device_shared_preferences" translatable="false">com.android.car.connecteddevice</string>
+<!-- THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY. -->
+<resources>
+    <overlayable name="PaintboothResources">
+        <policy type="public">
+            <item type="drawable" name="simulated_screen_shape"/>
+        </policy>
+    </overlayable>
 </resources>
diff --git a/car-ui-lib/paintbooth/src/main/res/values/strings.xml b/car-ui-lib/paintbooth/src/main/res/values/strings.xml
index 5965d68..b438835 100644
--- a/car-ui-lib/paintbooth/src/main/res/values/strings.xml
+++ b/car-ui-lib/paintbooth/src/main/res/values/strings.xml
@@ -63,6 +63,8 @@
   <string name="title_twoaction_preference">TwoAction preference</string>
   <!-- Summary of a two action preference [CHAR_LIMIT=70]-->
   <string name="summary_twoaction_preference">A widget should be visible on the right</string>
+  <!-- Summary of the deprecated two action preference [CHAR_LIMIT=70]-->
+  <string name="summary_deprecated_twoaction_preference">The deprecated version of TwoActionPreference</string>
 
   <!-- Title of a checkbox preference [CHAR_LIMIT=31]-->
   <string name="title_checkbox_preference">Checkbox preference</string>
diff --git a/car-ui-lib/paintbooth/src/main/res/xml/preference_samples.xml b/car-ui-lib/paintbooth/src/main/res/xml/preference_samples.xml
index d9a891c..4cc4ada 100644
--- a/car-ui-lib/paintbooth/src/main/res/xml/preference_samples.xml
+++ b/car-ui-lib/paintbooth/src/main/res/xml/preference_samples.xml
@@ -28,16 +28,6 @@
             android:title="@string/title_basic_preference"/>
 
         <Preference
-            android:key="preference_disabled_without_ripple"
-            android:summary="Without ripple"
-            android:title="@string/title_basic_preference"/>
-
-        <Preference
-            android:key="preference_disabled_with_ripple"
-            android:summary="With ripple"
-            android:title="@string/title_basic_preference"/>
-
-        <Preference
             android:key="stylized"
             android:dependency="preference"
             android:summary="@string/summary_stylish_preference"
@@ -89,9 +79,33 @@
 
         <com.android.car.ui.preference.CarUiTwoActionPreference
             android:key="twoaction"
-            android:summary="@string/summary_twoaction_preference"
+            android:summary="@string/summary_deprecated_twoaction_preference"
             android:title="@string/title_twoaction_preference"
             android:widgetLayout="@layout/details_preference_widget"/>
+
+        <com.android.car.ui.preference.CarUiTwoActionTextPreference
+            android:key="twoactiontext"
+            android:summary="@string/summary_twoaction_preference"
+            android:title="@string/title_twoaction_preference"
+            app:secondaryActionText="Secondary action"/>
+        <com.android.car.ui.preference.CarUiTwoActionTextPreference
+            android:key="twoactiontextborderless"
+            android:icon="@drawable/ic_settings_wifi"
+            android:summary="@string/summary_twoaction_preference"
+            android:title="@string/title_twoaction_preference"
+            app:secondaryActionStyle="borderless"
+            app:secondaryActionText="Secondary action"/>
+        <com.android.car.ui.preference.CarUiTwoActionIconPreference
+            android:key="twoactionicon"
+            android:icon="@drawable/ic_settings_wifi"
+            android:summary="@string/summary_twoaction_preference"
+            android:title="@string/title_twoaction_preference"
+            app:secondaryActionIcon="@drawable/ic_launcher"/>
+        <com.android.car.ui.preference.CarUiTwoActionSwitchPreference
+            android:key="twoactionswitch"
+            android:icon="@drawable/ic_settings_wifi"
+            android:summary="@string/summary_twoaction_preference"
+            android:title="@string/title_twoaction_preference"/>
     </PreferenceCategory>
 
     <PreferenceCategory
diff --git a/car-ui-lib/referencedesign/Android.mk b/car-ui-lib/referencedesign/Android.mk
index ef0ad97..974c19a 100644
--- a/car-ui-lib/referencedesign/Android.mk
+++ b/car-ui-lib/referencedesign/Android.mk
@@ -6,6 +6,7 @@
 CAR_UI_RESOURCE_DIR := $(LOCAL_PATH)/res
 CAR_UI_RRO_TARGETS := \
     com.android.car.ui.paintbooth \
+    com.google.android.car.ui.paintbooth \
     com.android.car.rotaryplayground \
     com.android.car.themeplayground \
     com.android.car.carlauncher \
diff --git a/car-ui-lib/referencedesign/product.mk b/car-ui-lib/referencedesign/product.mk
index e834714..0ccf70e 100644
--- a/car-ui-lib/referencedesign/product.mk
+++ b/car-ui-lib/referencedesign/product.mk
@@ -3,6 +3,7 @@
 # Include generated RROs
 PRODUCT_PACKAGES += \
     googlecarui-com-android-car-ui-paintbooth \
+    googlecarui-com-google-android-car-ui-paintbooth \
     googlecarui-com-android-car-rotaryplayground \
     googlecarui-com-android-car-themeplayground \
     googlecarui-com-android-car-carlauncher \
diff --git a/car-ui-lib/referencedesign/res/color/car_ui_text_color_primary.xml b/car-ui-lib/referencedesign/res/color/car_ui_text_color_primary.xml
index 34033e2..d310838 100644
--- a/car-ui-lib/referencedesign/res/color/car_ui_text_color_primary.xml
+++ b/car-ui-lib/referencedesign/res/color/car_ui_text_color_primary.xml
@@ -21,5 +21,8 @@
     <item android:state_enabled="false"
           android:alpha="?android:attr/disabledAlpha"
           android:color="?android:attr/colorForeground"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
     <item android:color="?android:attr/colorForeground"/>
 </selector>
diff --git a/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recycler_view.xml b/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recycler_view.xml
index 4847388..f487e35 100644
--- a/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recycler_view.xml
+++ b/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recycler_view.xml
@@ -18,11 +18,16 @@
 
     <com.android.car.ui.recyclerview.CarUiRecyclerViewContainer
         android:id="@+id/car_ui_recycler_view"
-        android:layout_width="0dp"
+        android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginStart="@dimen/car_ui_scrollbar_margin"
-        android:tag="carUiRecyclerView"
-        android:layout_weight="1"/>
+        android:paddingStart="@dimen/car_ui_scrollbar_margin"
+        android:paddingEnd="@dimen/car_ui_scrollbar_margin"
+        android:tag="carUiRecyclerView" />
 
-    <include layout="@layout/car_ui_recyclerview_scrollbar"/>
+    <FrameLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="left" >
+        <include layout="@layout/car_ui_recyclerview_scrollbar"/>
+    </FrameLayout>
 </merge>
diff --git a/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recyclerview_scrollbar.xml b/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recyclerview_scrollbar.xml
index 5896819..d5e626a 100644
--- a/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recyclerview_scrollbar.xml
+++ b/car-ui-lib/referencedesign/res/layout-ldrtl/car_ui_recyclerview_scrollbar.xml
@@ -45,7 +45,8 @@
         android:layout_height="0dp"
         android:layout_gravity="center_horizontal"
         android:background="@drawable/car_ui_recyclerview_scrollbar_thumb"
-        app:layout_constraintTop_toBottomOf="@+id/car_ui_scrollbar_page_up"
+        app:layout_constraintTop_toTopOf="@+id/car_ui_scrollbar_track"
+        app:layout_constraintBottom_toBottomOf="@+id/car_ui_scrollbar_track"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"/>
 
diff --git a/car-ui-lib/referencedesign/res/values/attrs.xml b/car-ui-lib/referencedesign/res/values/attrs.xml
index 0708c10..74adbf9 100644
--- a/car-ui-lib/referencedesign/res/values/attrs.xml
+++ b/car-ui-lib/referencedesign/res/values/attrs.xml
@@ -56,6 +56,6 @@
     <attr name="layout_goneMarginStart" format="dimension"/>
     <attr name="layout_goneMarginEnd" format="dimension"/>
 
-    <attr name="state_ux_restricted" format="boolean"/>
+    <attr name="state_ux_restricted"/>
 
 </resources>
diff --git a/car-ui-lib/referencedesign/res/values/styles.xml b/car-ui-lib/referencedesign/res/values/styles.xml
index b33d11a..c8d7e6e 100644
--- a/car-ui-lib/referencedesign/res/values/styles.xml
+++ b/car-ui-lib/referencedesign/res/values/styles.xml
@@ -72,7 +72,7 @@
     </style>
 
     <style name="TextAppearance.CarUi" parent="android:TextAppearance.DeviceDefault">
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textColor">@color/car_ui_text_color_primary</item>
         <item name="android:textAlignment">viewStart</item>
     </style>
 
diff --git a/car-ui-lib/referencedesign/res/xml/overlays.xml b/car-ui-lib/referencedesign/res/xml/overlays.xml
index 6332b67..a72420e 100644
--- a/car-ui-lib/referencedesign/res/xml/overlays.xml
+++ b/car-ui-lib/referencedesign/res/xml/overlays.xml
@@ -1,105 +1,93 @@
 <overlay>
-    <item target="layout/car_ui_base_layout_toolbar" value="@layout/car_ui_base_layout_toolbar"/>
-    <item target="layout/car_ui_toolbar" value="@layout/car_ui_toolbar"/>
-    <item target="layout/car_ui_toolbar_two_row" value="@layout/car_ui_toolbar_two_row"/>
-    <item target="layout/car_ui_toolbar_menu_item" value="@layout/car_ui_toolbar_menu_item"/>
-    <item target="layout/car_ui_toolbar_menu_item_primary" value="@layout/car_ui_toolbar_menu_item_primary"/>
-    <item target="layout/car_ui_preference_widget_seekbar" value="@layout/car_ui_preference_widget_seekbar"/>
+    <item target="attr/layout_constraintBottom_toBottomOf" value="@attr/layout_constraintBottom_toBottomOf"/>
+    <item target="attr/layout_constraintBottom_toTopOf" value="@attr/layout_constraintBottom_toTopOf"/>
+    <item target="attr/layout_constraintEnd_toEndOf" value="@attr/layout_constraintEnd_toEndOf"/>
+    <item target="attr/layout_constraintEnd_toStartOf" value="@attr/layout_constraintEnd_toStartOf"/>
+    <item target="attr/layout_constraintGuide_begin" value="@attr/layout_constraintGuide_begin"/>
+    <item target="attr/layout_constraintGuide_end" value="@attr/layout_constraintGuide_end"/>
+    <item target="attr/layout_constraintHorizontal_bias" value="@attr/layout_constraintHorizontal_bias"/>
+    <item target="attr/layout_constraintLeft_toLeftOf" value="@attr/layout_constraintLeft_toLeftOf"/>
+    <item target="attr/layout_constraintLeft_toRightOf" value="@attr/layout_constraintLeft_toRightOf"/>
+    <item target="attr/layout_constraintRight_toLeftOf" value="@attr/layout_constraintRight_toLeftOf"/>
+    <item target="attr/layout_constraintRight_toRightOf" value="@attr/layout_constraintRight_toRightOf"/>
+    <item target="attr/layout_constraintStart_toEndOf" value="@attr/layout_constraintStart_toEndOf"/>
+    <item target="attr/layout_constraintStart_toStartOf" value="@attr/layout_constraintStart_toStartOf"/>
+    <item target="attr/layout_constraintTop_toBottomOf" value="@attr/layout_constraintTop_toBottomOf"/>
+    <item target="attr/layout_constraintTop_toTopOf" value="@attr/layout_constraintTop_toTopOf"/>
+    <item target="attr/layout_goneMarginBottom" value="@attr/layout_goneMarginBottom"/>
+    <item target="attr/layout_goneMarginEnd" value="@attr/layout_goneMarginEnd"/>
+    <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginLeft"/>
+    <item target="attr/layout_goneMarginRight" value="@attr/layout_goneMarginRight"/>
+    <item target="attr/layout_goneMarginStart" value="@attr/layout_goneMarginStart"/>
+    <item target="attr/layout_goneMarginTop" value="@attr/layout_goneMarginTop"/>
+    <item target="attr/state_ux_restricted" value="@attr/state_ux_restricted"/>
 
-    <item target="drawable/car_ui_icon_arrow_back" value="@drawable/car_ui_icon_arrow_back"/>
-    <item target="drawable/car_ui_toolbar_menu_item_icon_ripple" value="@drawable/car_ui_toolbar_menu_item_icon_ripple"/>
-    <item target="drawable/car_ui_toolbar_menu_item_icon_background" value="@drawable/car_ui_toolbar_menu_item_icon_background"/>
-    <item target="drawable/car_ui_toolbar_menu_item_icon_background" value="@drawable/car_ui_toolbar_menu_item_icon_background"/>
-
-    <item target="style/Widget.CarUi" value="@style/Widget.CarUi"/>
-    <item target="style/Widget.CarUi.Toolbar" value="@style/Widget.CarUi.Toolbar"/>
-    <item target="style/Widget.CarUi.Toolbar.NavIconContainer" value="@style/Widget.CarUi.Toolbar.NavIconContainer"/>
-    <item target="style/Widget.CarUi.Toolbar.NavIcon" value="@style/Widget.CarUi.Toolbar.NavIcon"/>
-    <item target="style/Widget.CarUi.Toolbar.MenuItem" value="@style/Widget.CarUi.Toolbar.MenuItem"/>
-    <item target="style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer" value="@style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer"/>
-    <item target="style/Widget.CarUi.Button.Borderless.Colored" value="@style/Widget.CarUi.Button.Borderless.Colored"/>
-    <item target="style/Widget.CarUi.Toolbar.TextButton" value="@style/Widget.CarUi.Toolbar.TextButton"/>
-    <item target="style/Widget.CarUi.Toolbar.TextButton.WithIcon" value="@style/Widget.CarUi.Toolbar.TextButton.WithIcon"/>
-    <item target="style/Widget.CarUi.SeekbarPreference" value="@style/Widget.CarUi.SeekbarPreference"/>
-    <item target="style/Widget.CarUi.SeekbarPreference.Seekbar" value="@style/Widget.CarUi.SeekbarPreference.Seekbar"/>
-
-    <item target="dimen/car_ui_toolbar_margin" value="@dimen/car_ui_toolbar_margin"/>
+    <item target="bool/car_ui_toolbar_logo_fills_nav_icon_space" value="@bool/car_ui_toolbar_logo_fills_nav_icon_space" />
+    <item target="bool/car_ui_toolbar_menuitem_individual_click_listeners" value="@bool/car_ui_toolbar_menuitem_individual_click_listeners" />
+    <item target="bool/car_ui_toolbar_nav_icon_reserve_space" value="@bool/car_ui_toolbar_nav_icon_reserve_space" />
+    <item target="bool/car_ui_toolbar_tab_flexible_layout" value="@bool/car_ui_toolbar_tab_flexible_layout" />
+    <item target="bool/car_ui_toolbar_tabs_on_second_row" value="@bool/car_ui_toolbar_tabs_on_second_row" />
 
     <item target="color/car_ui_text_color_secondary" value="@color/car_ui_text_color_secondary"/>
     <item target="color/car_ui_toolbar_menu_item_icon_background_color" value="@color/car_ui_toolbar_menu_item_icon_background_color"/>
     <item target="color/car_ui_toolbar_menu_item_icon_color" value="@color/car_ui_toolbar_menu_item_icon_color"/>
 
+    <item target="dimen/car_ui_toolbar_margin" value="@dimen/car_ui_toolbar_margin"/>
+
     <item target="drawable/car_ui_icon_arrow_back" value="@drawable/car_ui_icon_arrow_back"/>
+    <item target="drawable/car_ui_toolbar_menu_item_icon_background" value="@drawable/car_ui_toolbar_menu_item_icon_background"/>
     <item target="drawable/car_ui_toolbar_menu_item_icon_ripple" value="@drawable/car_ui_toolbar_menu_item_icon_ripple"/>
 
-    <item target="style/TextAppearance.CarUi" value="@style/TextAppearance.CarUi"/>
-    <item target="style/TextAppearance.CarUi.Body1" value="@style/TextAppearance.CarUi.Body1"/>
-    <item target="style/TextAppearance.CarUi.PreferenceTitle" value="@style/TextAppearance.CarUi.PreferenceTitle"/>
-    <item target="style/TextAppearance.CarUi.Body3" value="@style/TextAppearance.CarUi.Body3"/>
-    <item target="style/TextAppearance.CarUi.PreferenceSummary" value="@style/TextAppearance.CarUi.PreferenceSummary"/>
-
-    <item target="style/Widget.CarUi" value="@style/Widget.CarUi"/>
-    <item target="style/Widget.CarUi.Toolbar" value="@style/Widget.CarUi.Toolbar"/>
-    <item target="style/Widget.CarUi.Toolbar.NavIconContainer" value="@style/Widget.CarUi.Toolbar.NavIconContainer"/>
-    <item target="style/Widget.CarUi.Toolbar.NavIcon" value="@style/Widget.CarUi.Toolbar.NavIcon"/>
-
-    <item target="dimen/car_ui_toolbar_margin" value="@dimen/car_ui_toolbar_margin"/>
-    <item target="layout/car_ui_recycler_view" value="@layout/car_ui_recycler_view"/>
-
-    <item target="bool/car_ui_toolbar_nav_icon_reserve_space" value="@bool/car_ui_toolbar_nav_icon_reserve_space" />
-    <item target="bool/car_ui_toolbar_logo_fills_nav_icon_space" value="@bool/car_ui_toolbar_logo_fills_nav_icon_space" />
-    <item target="bool/car_ui_toolbar_tab_flexible_layout" value="@bool/car_ui_toolbar_tab_flexible_layout" />
-    <item target="bool/car_ui_toolbar_tabs_on_second_row" value="@bool/car_ui_toolbar_tabs_on_second_row" />
-    <item target="bool/car_ui_toolbar_menuitem_individual_click_listeners" value="@bool/car_ui_toolbar_menuitem_individual_click_listeners" />
-
-    <item target="id/car_ui_toolbar_background" value="@id/car_ui_toolbar_background" />
-    <item target="id/car_ui_toolbar_nav_icon_container" value="@id/car_ui_toolbar_nav_icon_container" />
-    <item target="id/car_ui_toolbar_nav_icon" value="@id/car_ui_toolbar_nav_icon" />
-    <item target="id/car_ui_toolbar_logo" value="@id/car_ui_toolbar_logo" />
-    <item target="id/car_ui_toolbar_title_logo_container" value="@id/car_ui_toolbar_title_logo_container" />
-    <item target="id/car_ui_toolbar_title_logo" value="@id/car_ui_toolbar_title_logo" />
-    <item target="id/car_ui_toolbar_title" value="@id/car_ui_toolbar_title" />
-    <item target="id/car_ui_toolbar_title_container" value="@id/car_ui_toolbar_title_container" />
-    <item target="id/car_ui_toolbar_subtitle" value="@id/car_ui_toolbar_subtitle" />
-    <item target="id/car_ui_toolbar_tabs" value="@id/car_ui_toolbar_tabs" />
-    <item target="id/car_ui_toolbar_menu_items_container" value="@id/car_ui_toolbar_menu_items_container" />
-    <item target="id/car_ui_toolbar_search_view_container" value="@id/car_ui_toolbar_search_view_container" />
-    <item target="id/car_ui_toolbar_progress_bar" value="@id/car_ui_toolbar_progress_bar" />
     <item target="id/car_ui_base_layout_content_container" value="@id/car_ui_base_layout_content_container" />
-    <item target="id/car_ui_toolbar_menu_item_icon_container" value="@id/car_ui_toolbar_menu_item_icon_container"/>
-    <item target="id/car_ui_toolbar_menu_item_icon" value="@id/car_ui_toolbar_menu_item_icon"/>
-    <item target="id/car_ui_toolbar_menu_item_switch" value="@id/car_ui_toolbar_menu_item_switch"/>
-    <item target="id/car_ui_toolbar_menu_item_text" value="@id/car_ui_toolbar_menu_item_text"/>
-    <item target="id/car_ui_toolbar_menu_item_text_with_icon" value="@id/car_ui_toolbar_menu_item_text_with_icon"/>
-    <item target="id/seekbar" value="@id/seekbar"/>
-    <item target="id/seekbar_value" value="@id/seekbar_value"/>
     <item target="id/car_ui_recycler_view" value="@id/car_ui_recycler_view" />
-    <item target="id/car_ui_scroll_bar" value="@id/car_ui_scroll_bar"/>
     <item target="id/car_ui_scrollbar_page_down" value="@id/car_ui_scrollbar_page_down"/>
     <item target="id/car_ui_scrollbar_page_up" value="@id/car_ui_scrollbar_page_up"/>
     <item target="id/car_ui_scrollbar_thumb" value="@id/car_ui_scrollbar_thumb"/>
     <item target="id/car_ui_scrollbar_track" value="@id/car_ui_scrollbar_track"/>
+    <item target="id/car_ui_scroll_bar" value="@id/car_ui_scroll_bar"/>
+    <item target="id/car_ui_toolbar_background" value="@id/car_ui_toolbar_background" />
+    <item target="id/car_ui_toolbar_logo" value="@id/car_ui_toolbar_logo" />
+    <item target="id/car_ui_toolbar_menu_item_icon_container" value="@id/car_ui_toolbar_menu_item_icon_container"/>
+    <item target="id/car_ui_toolbar_menu_item_icon" value="@id/car_ui_toolbar_menu_item_icon"/>
+    <item target="id/car_ui_toolbar_menu_items_container" value="@id/car_ui_toolbar_menu_items_container" />
+    <item target="id/car_ui_toolbar_menu_item_switch" value="@id/car_ui_toolbar_menu_item_switch"/>
+    <item target="id/car_ui_toolbar_menu_item_text" value="@id/car_ui_toolbar_menu_item_text"/>
+    <item target="id/car_ui_toolbar_menu_item_text_with_icon" value="@id/car_ui_toolbar_menu_item_text_with_icon"/>
+    <item target="id/car_ui_toolbar_nav_icon_container" value="@id/car_ui_toolbar_nav_icon_container" />
+    <item target="id/car_ui_toolbar_nav_icon" value="@id/car_ui_toolbar_nav_icon" />
+    <item target="id/car_ui_toolbar_progress_bar" value="@id/car_ui_toolbar_progress_bar" />
+    <item target="id/car_ui_toolbar_search_view_container" value="@id/car_ui_toolbar_search_view_container" />
+    <item target="id/car_ui_toolbar_subtitle" value="@id/car_ui_toolbar_subtitle" />
+    <item target="id/car_ui_toolbar_tabs" value="@id/car_ui_toolbar_tabs" />
+    <item target="id/car_ui_toolbar_title_container" value="@id/car_ui_toolbar_title_container" />
+    <item target="id/car_ui_toolbar_title_logo_container" value="@id/car_ui_toolbar_title_logo_container" />
+    <item target="id/car_ui_toolbar_title_logo" value="@id/car_ui_toolbar_title_logo" />
+    <item target="id/car_ui_toolbar_title" value="@id/car_ui_toolbar_title" />
+    <item target="id/seekbar" value="@id/seekbar"/>
+    <item target="id/seekbar_value" value="@id/seekbar_value"/>
 
-    <item target="attr/layout_constraintGuide_begin" value="@attr/layout_constraintGuide_begin"/>
-    <item target="attr/layout_constraintGuide_end" value="@attr/layout_constraintGuide_end"/>
-    <item target="attr/layout_constraintStart_toStartOf" value="@attr/layout_constraintStart_toStartOf"/>
-    <item target="attr/layout_constraintStart_toEndOf" value="@attr/layout_constraintStart_toEndOf"/>
-    <item target="attr/layout_constraintEnd_toStartOf" value="@attr/layout_constraintEnd_toStartOf"/>
-    <item target="attr/layout_constraintEnd_toEndOf" value="@attr/layout_constraintEnd_toEndOf"/>
-    <item target="attr/layout_constraintLeft_toLeftOf" value="@attr/layout_constraintLeft_toLeftOf"/>
-    <item target="attr/layout_constraintLeft_toRightOf" value="@attr/layout_constraintLeft_toRightOf"/>
-    <item target="attr/layout_constraintRight_toLeftOf" value="@attr/layout_constraintRight_toLeftOf"/>
-    <item target="attr/layout_constraintRight_toRightOf" value="@attr/layout_constraintRight_toRightOf"/>
-    <item target="attr/layout_constraintTop_toTopOf" value="@attr/layout_constraintTop_toTopOf"/>
-    <item target="attr/layout_constraintTop_toBottomOf" value="@attr/layout_constraintTop_toBottomOf"/>
-    <item target="attr/layout_constraintBottom_toTopOf" value="@attr/layout_constraintBottom_toTopOf"/>
-    <item target="attr/layout_constraintBottom_toBottomOf" value="@attr/layout_constraintBottom_toBottomOf"/>
-    <item target="attr/layout_constraintHorizontal_bias" value="@attr/layout_constraintHorizontal_bias"/>
-    <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginLeft"/>
-    <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginRight"/>
-    <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginTop"/>
-    <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginBottom"/>
-    <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginStart"/>
-    <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginEnd"/>
-    <item target="attr/state_ux_restricted" value="@attr/state_ux_restricted"/>
+    <item target="layout/car_ui_base_layout_toolbar" value="@layout/car_ui_base_layout_toolbar"/>
+    <item target="layout/car_ui_preference_widget_seekbar" value="@layout/car_ui_preference_widget_seekbar"/>
+    <item target="layout/car_ui_recycler_view" value="@layout/car_ui_recycler_view"/>
+    <item target="layout/car_ui_toolbar_menu_item_primary" value="@layout/car_ui_toolbar_menu_item_primary"/>
+    <item target="layout/car_ui_toolbar_menu_item" value="@layout/car_ui_toolbar_menu_item"/>
+    <item target="layout/car_ui_toolbar_two_row" value="@layout/car_ui_toolbar_two_row"/>
+    <item target="layout/car_ui_toolbar" value="@layout/car_ui_toolbar"/>
+
+    <item target="style/TextAppearance.CarUi.Body1" value="@style/TextAppearance.CarUi.Body1"/>
+    <item target="style/TextAppearance.CarUi.Body3" value="@style/TextAppearance.CarUi.Body3"/>
+    <item target="style/TextAppearance.CarUi.PreferenceSummary" value="@style/TextAppearance.CarUi.PreferenceSummary"/>
+    <item target="style/TextAppearance.CarUi.PreferenceTitle" value="@style/TextAppearance.CarUi.PreferenceTitle"/>
+    <item target="style/TextAppearance.CarUi" value="@style/TextAppearance.CarUi"/>
+    <item target="style/Widget.CarUi.Button.Borderless.Colored" value="@style/Widget.CarUi.Button.Borderless.Colored"/>
+    <item target="style/Widget.CarUi.SeekbarPreference.Seekbar" value="@style/Widget.CarUi.SeekbarPreference.Seekbar"/>
+    <item target="style/Widget.CarUi.SeekbarPreference" value="@style/Widget.CarUi.SeekbarPreference"/>
+    <item target="style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer" value="@style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer"/>
+    <item target="style/Widget.CarUi.Toolbar.MenuItem" value="@style/Widget.CarUi.Toolbar.MenuItem"/>
+    <item target="style/Widget.CarUi.Toolbar.NavIconContainer" value="@style/Widget.CarUi.Toolbar.NavIconContainer"/>
+    <item target="style/Widget.CarUi.Toolbar.NavIcon" value="@style/Widget.CarUi.Toolbar.NavIcon"/>
+    <item target="style/Widget.CarUi.Toolbar.TextButton" value="@style/Widget.CarUi.Toolbar.TextButton"/>
+    <item target="style/Widget.CarUi.Toolbar.TextButton.WithIcon" value="@style/Widget.CarUi.Toolbar.TextButton.WithIcon"/>
+    <item target="style/Widget.CarUi.Toolbar" value="@style/Widget.CarUi.Toolbar"/>
+    <item target="style/Widget.CarUi" value="@style/Widget.CarUi"/>
 </overlay>
diff --git a/car-ui-lib/tests/apitest/current.xml b/car-ui-lib/tests/apitest/current.xml
index 769b040..674d8b4 100644
--- a/car-ui-lib/tests/apitest/current.xml
+++ b/car-ui-lib/tests/apitest/current.xml
@@ -15,6 +15,7 @@
   <public type="bool" name="car_ui_ime_wide_screen_aligned_left"/>
   <public type="bool" name="car_ui_ime_wide_screen_allow_app_hide_content_area"/>
   <public type="bool" name="car_ui_list_item_single_line_title"/>
+  <public type="bool" name="car_ui_preference_list_instant_change_callback"/>
   <public type="bool" name="car_ui_preference_list_show_full_screen"/>
   <public type="bool" name="car_ui_preference_show_chevron"/>
   <public type="bool" name="car_ui_scrollbar_enable"/>
@@ -82,6 +83,7 @@
   <public type="dimen" name="car_ui_ime_wide_screen_error_text_size"/>
   <public type="dimen" name="car_ui_ime_wide_screen_input_area_height"/>
   <public type="dimen" name="car_ui_ime_wide_screen_input_area_margin_top"/>
+  <public type="dimen" name="car_ui_ime_wide_screen_input_area_padding_end"/>
   <public type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_left"/>
   <public type="dimen" name="car_ui_ime_wide_screen_input_edit_text_padding_right"/>
   <public type="dimen" name="car_ui_ime_wide_screen_input_edit_text_size"/>
@@ -244,26 +246,39 @@
   <public type="drawable" name="car_ui_toolbar_menu_item_icon_ripple"/>
   <public type="drawable" name="car_ui_toolbar_search_close_icon"/>
   <public type="drawable" name="car_ui_toolbar_search_search_icon"/>
-  <public type="id" name="action_container"/>
-  <public type="id" name="action_container_touch_interceptor"/>
-  <public type="id" name="action_divider"/>
   <public type="id" name="action_widget_container"/>
-  <public type="id" name="avatar_icon"/>
-  <public type="id" name="body"/>
   <public type="id" name="car_ui_alert_icon"/>
   <public type="id" name="car_ui_alert_subtitle"/>
   <public type="id" name="car_ui_alert_title"/>
   <public type="id" name="car_ui_base_layout_content_container"/>
   <public type="id" name="car_ui_closeKeyboard"/>
   <public type="id" name="car_ui_contentAreaAutomotive"/>
+  <public type="id" name="car_ui_divider"/>
+  <public type="id" name="car_ui_first_action_container"/>
   <public type="id" name="car_ui_focus_area"/>
   <public type="id" name="car_ui_fullscreenArea"/>
   <public type="id" name="car_ui_imeWideScreenInputArea"/>
   <public type="id" name="car_ui_ime_surface"/>
   <public type="id" name="car_ui_inputExtractActionAutomotive"/>
   <public type="id" name="car_ui_inputExtractEditTextContainer"/>
+  <public type="id" name="car_ui_list_item_action_container"/>
+  <public type="id" name="car_ui_list_item_action_container_touch_interceptor"/>
+  <public type="id" name="car_ui_list_item_action_divider"/>
+  <public type="id" name="car_ui_list_item_avatar_icon"/>
+  <public type="id" name="car_ui_list_item_body"/>
+  <public type="id" name="car_ui_list_item_checkbox_widget"/>
+  <public type="id" name="car_ui_list_item_content_icon"/>
   <public type="id" name="car_ui_list_item_end_guideline"/>
+  <public type="id" name="car_ui_list_item_icon"/>
+  <public type="id" name="car_ui_list_item_icon_container"/>
+  <public type="id" name="car_ui_list_item_radio_button_widget"/>
+  <public type="id" name="car_ui_list_item_reduced_touch_interceptor"/>
   <public type="id" name="car_ui_list_item_start_guideline"/>
+  <public type="id" name="car_ui_list_item_supplemental_icon"/>
+  <public type="id" name="car_ui_list_item_switch_widget"/>
+  <public type="id" name="car_ui_list_item_text_container"/>
+  <public type="id" name="car_ui_list_item_title"/>
+  <public type="id" name="car_ui_list_item_touch_interceptor"/>
   <public type="id" name="car_ui_list_limiting_message"/>
   <public type="id" name="car_ui_preference_container_without_widget"/>
   <public type="id" name="car_ui_preference_fragment_container"/>
@@ -273,6 +288,9 @@
   <public type="id" name="car_ui_scrollbar_page_up"/>
   <public type="id" name="car_ui_scrollbar_thumb"/>
   <public type="id" name="car_ui_scrollbar_track"/>
+  <public type="id" name="car_ui_second_action_container"/>
+  <public type="id" name="car_ui_secondary_action"/>
+  <public type="id" name="car_ui_secondary_action_concrete"/>
   <public type="id" name="car_ui_toolbar"/>
   <public type="id" name="car_ui_toolbar_background"/>
   <public type="id" name="car_ui_toolbar_bottom_guideline"/>
@@ -312,17 +330,11 @@
   <public type="id" name="car_ui_wideScreenExtractedTextIcon"/>
   <public type="id" name="car_ui_wideScreenInputArea"/>
   <public type="id" name="car_ui_wideScreenSearchResultList"/>
-  <public type="id" name="checkbox_widget"/>
   <public type="id" name="container"/>
-  <public type="id" name="content_icon"/>
-  <public type="id" name="icon"/>
-  <public type="id" name="icon_container"/>
   <public type="id" name="list"/>
   <public type="id" name="nested_recycler_view_layout"/>
   <public type="id" name="radio_button"/>
-  <public type="id" name="radio_button_widget"/>
   <public type="id" name="recycler_view"/>
-  <public type="id" name="reduced_touch_interceptor"/>
   <public type="id" name="search"/>
   <public type="id" name="seek_bar"/>
   <public type="id" name="seek_bar_text_left"/>
@@ -331,14 +343,9 @@
   <public type="id" name="seekbar"/>
   <public type="id" name="seekbar_value"/>
   <public type="id" name="spinner"/>
-  <public type="id" name="supplemental_icon"/>
-  <public type="id" name="switch_widget"/>
-  <public type="id" name="text_container"/>
   <public type="id" name="textbox"/>
-  <public type="id" name="title"/>
   <public type="id" name="title_template"/>
   <public type="id" name="toolbar"/>
-  <public type="id" name="touch_interceptor"/>
   <public type="integer" name="car_ui_default_max_string_length"/>
   <public type="integer" name="car_ui_focus_area_history_cache_type"/>
   <public type="integer" name="car_ui_focus_area_history_expiration_period_ms"/>
@@ -365,6 +372,10 @@
   <public type="layout" name="car_ui_preference_dropdown"/>
   <public type="layout" name="car_ui_preference_fragment"/>
   <public type="layout" name="car_ui_preference_fragment_with_toolbar"/>
+  <public type="layout" name="car_ui_preference_two_action_icon"/>
+  <public type="layout" name="car_ui_preference_two_action_switch"/>
+  <public type="layout" name="car_ui_preference_two_action_text"/>
+  <public type="layout" name="car_ui_preference_two_action_text_borderless"/>
   <public type="layout" name="car_ui_preference_widget_checkbox"/>
   <public type="layout" name="car_ui_preference_widget_seekbar"/>
   <public type="layout" name="car_ui_preference_widget_switch"/>
diff --git a/connected-device-lib/Android.bp b/connected-device-lib/Android.bp
deleted file mode 100644
index 4383a20..0000000
--- a/connected-device-lib/Android.bp
+++ /dev/null
@@ -1,43 +0,0 @@
-//
-// Copyright (C) 2019 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.
-//
-
-android_library {
-    name: "connected-device-lib",
-
-    srcs: ["src/**/*.java"],
-
-    manifest: "AndroidManifest.xml",
-
-    optimize: {
-        enabled: false,
-    },
-
-    libs: ["android.car-system-stubs"],
-
-    static_libs: [
-        "androidx.room_room-runtime",
-        "connected-device-protos",
-        "encryption-runner",
-        "guava",
-    ],
-
-    plugins: [
-        "androidx.room_room-compiler-plugin",
-    ],
-
-    sdk_version: "system_current",
-    min_sdk_version: "28",
-}
diff --git a/connected-device-lib/AndroidManifest.xml b/connected-device-lib/AndroidManifest.xml
deleted file mode 100644
index d02ffce..0000000
--- a/connected-device-lib/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ Copyright (C) 2019 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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.car.connecteddevice">
-
-  <!--  Needed for BLE scanning/advertising -->
-  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-  <uses-permission android:name="android.permission.BLUETOOTH"/>
-  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
-
-  <!--  Needed for detecting foreground user -->
-  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
-  <uses-permission android:name="android.permission.MANAGE_USERS" />
-</manifest>
diff --git a/connected-device-lib/OWNERS b/connected-device-lib/OWNERS
deleted file mode 100644
index 108da4e..0000000
--- a/connected-device-lib/OWNERS
+++ /dev/null
@@ -1,5 +0,0 @@
-# People who can approve changes for submission.
-nicksauer@google.com
-ramperry@google.com
-ajchen@google.com
-danharms@google.com
diff --git a/connected-device-lib/proto/Android.bp b/connected-device-lib/proto/Android.bp
deleted file mode 100644
index c9dcb73..0000000
--- a/connected-device-lib/proto/Android.bp
+++ /dev/null
@@ -1,26 +0,0 @@
-//
-// Copyright (C) 2019 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.
-//
-
-java_library_static {
-    name: "connected-device-protos",
-    host_supported: true,
-    proto: {
-        type: "lite",
-    },
-    srcs: ["*.proto"],
-    jarjar_rules: "jarjar-rules.txt",
-    sdk_version: "28",
-}
diff --git a/connected-device-lib/proto/device_message.proto b/connected-device-lib/proto/device_message.proto
deleted file mode 100644
index 79b98df..0000000
--- a/connected-device-lib/proto/device_message.proto
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2019 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.
- */
-
-syntax = "proto3";
-
-package com.android.car.connecteddevice.proto;
-
-import "packages/apps/Car/libs/connected-device-lib/proto/operation_type.proto";
-
-option java_package = "com.android.car.connecteddevice.StreamProtos";
-option java_outer_classname = "DeviceMessageProto";
-
-// A message between devices.
-message Message {
-  // The operation that this message represents.
-  OperationType operation = 1;
-
-  // Whether the payload field is encrypted.
-  bool is_payload_encrypted = 2;
-
-  // Identifier of the intended recipient.
-  bytes recipient = 3;
-
-  // The bytes that represent the content for this message.
-  bytes payload = 4;
-}
\ No newline at end of file
diff --git a/connected-device-lib/proto/jarjar-rules.txt b/connected-device-lib/proto/jarjar-rules.txt
deleted file mode 100644
index d27aecb..0000000
--- a/connected-device-lib/proto/jarjar-rules.txt
+++ /dev/null
@@ -1 +0,0 @@
-rule com.google.protobuf.** com.android.car.protobuf.@1
diff --git a/connected-device-lib/proto/operation_type.proto b/connected-device-lib/proto/operation_type.proto
deleted file mode 100644
index 3e38e27..0000000
--- a/connected-device-lib/proto/operation_type.proto
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2019 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.
- */
-
-syntax = "proto3";
-
-package com.android.car.connecteddevice.proto;
-
-option java_package = "com.android.car.connecteddevice.StreamProtos";
-option java_outer_classname = "OperationProto";
-
-// The different message types that indicate the content of the payload.
-//
-// Ensure that these values are positive to reduce incurring too many bytes
-// to encode.
-enum OperationType {
-  // The contents of the payload are unknown.
-  //
-  // Note, this enum name is prefixed. See
-  // go/proto-best-practices-checkers#enum-default-value-name-conflict
-  OPERATION_TYPE_UNKNOWN = 0;
-
-  // The payload contains handshake messages needed to set up encryption.
-  ENCRYPTION_HANDSHAKE = 2;
-
-  // The message is an acknowledgment of a previously received message. The
-  // payload for this type should be empty.
-  ACK = 3;
-
-  // The payload contains a client-specific message.
-  CLIENT_MESSAGE = 4;
-}
diff --git a/connected-device-lib/proto/packet.proto b/connected-device-lib/proto/packet.proto
deleted file mode 100644
index f761622..0000000
--- a/connected-device-lib/proto/packet.proto
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2019 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.
- */
-
-syntax = "proto3";
-
-package com.android.car.connecteddevice.proto;
-
-option java_package = "com.android.car.connecteddevice.StreamProtos";
-option java_outer_classname = "PacketProto";
-
-// A packet across a stream.
-message Packet {
-  // A 1-based packet number. The first message will have a value of "1" rather
-  // than "0".
-  fixed32 packet_number = 1;
-
-  // The total number of packets in the message stream.
-  int32 total_packets = 2;
-
-  // Id of message for reassembly on other side
-  int32 message_id = 3;
-
-  // The bytes that represent the message content for this packet.
-  bytes payload = 4;
-}
diff --git a/connected-device-lib/proto/version_exchange.proto b/connected-device-lib/proto/version_exchange.proto
deleted file mode 100644
index a7c3493..0000000
--- a/connected-device-lib/proto/version_exchange.proto
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2019 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.
- */
-
-syntax = "proto3";
-
-package com.android.car.connecteddevice.proto;
-
-option java_package = "com.android.car.connecteddevice.StreamProtos";
-option java_outer_classname = "VersionExchangeProto";
-
-message VersionExchange {
-  // Minimum supported protobuf version.
-  int32 minSupportedMessagingVersion = 1;
-
-  // Maximum supported protobuf version.
-  int32 maxSupportedMessagingVersion = 2;
-
-  // Minimum supported version of the encryption engine.
-  int32 minSupportedSecurityVersion = 3;
-
-  // Maximum supported version of the encryption engine.
-  int32 maxSupportedSecurityVersion = 4;
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/AssociationCallback.java b/connected-device-lib/src/com/android/car/connecteddevice/AssociationCallback.java
deleted file mode 100644
index 7697131..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/AssociationCallback.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice;
-
-import androidx.annotation.NonNull;
-
-/** Callbacks that will be invoked during associating a new client. */
-public interface AssociationCallback {
-
-    /**
-     * Invoked when IHU starts advertising with its device name for association successfully.
-     *
-     * @param deviceName The device name to identify the car.
-     */
-    default void onAssociationStartSuccess(String deviceName) {}
-
-    /** Invoked when IHU failed to start advertising for association. */
-    default void onAssociationStartFailure() {}
-
-    /**
-     * Invoked when a {@link ConnectedDeviceManager.DeviceError} has been encountered in attempting
-     * to associate a new device.
-     *
-     * @param error The failure indication.
-     */
-    default void onAssociationError(@ConnectedDeviceManager.DeviceError int error) {}
-
-    /**
-     * Invoked when a verification code needs to be displayed. The user needs to confirm, and
-     * then call {@link ConnectedDeviceManager#notifyOutOfBandAccepted()}.
-     *
-     * @param code The verification code.
-     */
-    default void onVerificationCodeAvailable(@NonNull String code) {}
-
-    /**
-     * Invoked when the association has completed.
-     *
-     * @param deviceId The id of the newly associated device.
-     */
-    default void onAssociationCompleted(@NonNull String deviceId) {}
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java
deleted file mode 100644
index b1397e2..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java
+++ /dev/null
@@ -1,905 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.connection.DeviceMessage;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.model.ConnectedDevice;
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-import com.android.car.connecteddevice.oob.BluetoothRfcommChannel;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage.AssociatedDeviceCallback;
-import com.android.car.connecteddevice.util.ByteUtils;
-import com.android.car.connecteddevice.util.EventLog;
-import com.android.car.connecteddevice.util.ThreadSafeCallbacks;
-
-import java.lang.annotation.Retention;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-
-/** Manager of devices connected to the car. */
-public class ConnectedDeviceManager {
-
-    private static final String TAG = "ConnectedDeviceManager";
-
-    // Device name length is limited by available bytes in BLE advertisement data packet.
-    //
-    // BLE advertisement limits data packet length to 31
-    // Currently we send:
-    // - 18 bytes for 16 chars UUID: 16 bytes + 2 bytes for header;
-    // - 3 bytes for advertisement being connectable;
-    // which leaves 10 bytes.
-    // Subtracting 2 bytes used by header, we have 8 bytes for device name.
-    private static final int DEVICE_NAME_LENGTH_LIMIT = 8;
-
-    private final ConnectedDeviceStorage mStorage;
-
-    private final CarBluetoothManager mCarBluetoothManager;
-
-    private final ThreadSafeCallbacks<DeviceAssociationCallback> mDeviceAssociationCallbacks =
-            new ThreadSafeCallbacks<>();
-
-    private final ThreadSafeCallbacks<ConnectionCallback> mActiveUserConnectionCallbacks =
-            new ThreadSafeCallbacks<>();
-
-    private final ThreadSafeCallbacks<ConnectionCallback> mAllUserConnectionCallbacks =
-            new ThreadSafeCallbacks<>();
-
-    // deviceId -> (recipientId -> callbacks)
-    private final Map<String, Map<UUID, ThreadSafeCallbacks<DeviceCallback>>> mDeviceCallbacks =
-            new ConcurrentHashMap<>();
-
-    // deviceId -> device
-    private final Map<String, ConnectedDevice> mConnectedDevices =
-            new ConcurrentHashMap<>();
-
-    // recipientId -> (deviceId -> message bytes)
-    private final Map<UUID, Map<String, List<byte[]>>> mRecipientMissedMessages =
-            new ConcurrentHashMap<>();
-
-    // Recipient ids that received multiple callback registrations indicate that the recipient id
-    // has been compromised. Another party now has access the messages intended for that recipient.
-    // As a safeguard, that recipient id will be added to this list and blocked from further
-    // callback notifications.
-    private final Set<UUID> mBlacklistedRecipients = new CopyOnWriteArraySet<>();
-
-    private final AtomicBoolean mIsConnectingToUserDevice = new AtomicBoolean(false);
-
-    private final AtomicBoolean mHasStarted = new AtomicBoolean(false);
-
-    private String mNameForAssociation;
-
-    private AssociationCallback mAssociationCallback;
-
-    private MessageDeliveryDelegate mMessageDeliveryDelegate;
-
-    private OobChannel mOobChannel;
-
-    @Retention(SOURCE)
-    @IntDef({
-            DEVICE_ERROR_INVALID_HANDSHAKE,
-            DEVICE_ERROR_INVALID_MSG,
-            DEVICE_ERROR_INVALID_DEVICE_ID,
-            DEVICE_ERROR_INVALID_VERIFICATION,
-            DEVICE_ERROR_INVALID_CHANNEL_STATE,
-            DEVICE_ERROR_INVALID_ENCRYPTION_KEY,
-            DEVICE_ERROR_STORAGE_FAILURE,
-            DEVICE_ERROR_INVALID_SECURITY_KEY,
-            DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED,
-            DEVICE_ERROR_UNEXPECTED_DISCONNECTION
-    })
-    public @interface DeviceError {}
-
-    public static final int DEVICE_ERROR_INVALID_HANDSHAKE = 0;
-    public static final int DEVICE_ERROR_INVALID_MSG = 1;
-    public static final int DEVICE_ERROR_INVALID_DEVICE_ID = 2;
-    public static final int DEVICE_ERROR_INVALID_VERIFICATION = 3;
-    public static final int DEVICE_ERROR_INVALID_CHANNEL_STATE = 4;
-    public static final int DEVICE_ERROR_INVALID_ENCRYPTION_KEY = 5;
-    public static final int DEVICE_ERROR_STORAGE_FAILURE = 6;
-    public static final int DEVICE_ERROR_INVALID_SECURITY_KEY = 7;
-    public static final int DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED = 8;
-    public static final int DEVICE_ERROR_UNEXPECTED_DISCONNECTION = 9;
-
-    public ConnectedDeviceManager(@NonNull CarBluetoothManager carBluetoothManager,
-            @NonNull ConnectedDeviceStorage storage) {
-        Executor callbackExecutor = Executors.newSingleThreadExecutor();
-        mStorage = storage;
-        mCarBluetoothManager = carBluetoothManager;
-        mCarBluetoothManager.registerCallback(generateCarBleCallback(carBluetoothManager),
-                callbackExecutor);
-        mStorage.setAssociatedDeviceCallback(mAssociatedDeviceCallback);
-        logd(TAG, "ConnectedDeviceManager created successfully.");
-    }
-
-    /**
-     * Start internal processes and begin discovering devices. Must be called before any
-     * connections can be made using {@link #connectToActiveUserDevice()}.
-     */
-    public void start() {
-        if (mHasStarted.getAndSet(true)) {
-            reset();
-        } else {
-            logd(TAG, "Starting ConnectedDeviceManager.");
-            EventLog.onConnectedDeviceManagerStarted();
-        }
-        mCarBluetoothManager.start();
-        connectToActiveUserDevice();
-    }
-
-    /** Reset internal processes and disconnect any active connections. */
-    public void reset() {
-        logd(TAG, "Resetting ConnectedDeviceManager.");
-        for (ConnectedDevice device : mConnectedDevices.values()) {
-            removeConnectedDevice(device.getDeviceId());
-        }
-        mCarBluetoothManager.stop();
-        mIsConnectingToUserDevice.set(false);
-        if (mOobChannel != null) {
-            mOobChannel.interrupt();
-            mOobChannel = null;
-        }
-        mAssociationCallback = null;
-    }
-
-    /** Returns {@link List<ConnectedDevice>} of devices currently connected. */
-    @NonNull
-    public List<ConnectedDevice> getActiveUserConnectedDevices() {
-        List<ConnectedDevice> activeUserConnectedDevices = new ArrayList<>();
-        for (ConnectedDevice device : mConnectedDevices.values()) {
-            if (device.isAssociatedWithActiveUser()) {
-                activeUserConnectedDevices.add(device);
-            }
-        }
-        logd(TAG, "Returned " + activeUserConnectedDevices.size() + " active user devices.");
-        return activeUserConnectedDevices;
-    }
-
-    /**
-     * Register a callback for triggered associated device related events.
-     *
-     * @param callback {@link DeviceAssociationCallback} to register.
-     * @param executor {@link Executor} to execute triggers on.
-     */
-    public void registerDeviceAssociationCallback(@NonNull DeviceAssociationCallback callback,
-            @NonNull Executor executor) {
-        mDeviceAssociationCallbacks.add(callback, executor);
-    }
-
-    /**
-     * Unregister a device association callback.
-     *
-     * @param callback {@link DeviceAssociationCallback} to unregister.
-     */
-    public void unregisterDeviceAssociationCallback(@NonNull DeviceAssociationCallback callback) {
-        mDeviceAssociationCallbacks.remove(callback);
-    }
-
-    /**
-     * Register a callback for manager triggered connection events for only the currently active
-     * user's devices.
-     *
-     * @param callback {@link ConnectionCallback} to register.
-     * @param executor {@link Executor} to execute triggers on.
-     */
-    public void registerActiveUserConnectionCallback(@NonNull ConnectionCallback callback,
-            @NonNull Executor executor) {
-        mActiveUserConnectionCallbacks.add(callback, executor);
-    }
-
-    /**
-     * Unregister a connection callback from manager.
-     *
-     * @param callback {@link ConnectionCallback} to unregister.
-     */
-    public void unregisterConnectionCallback(ConnectionCallback callback) {
-        mActiveUserConnectionCallbacks.remove(callback);
-        mAllUserConnectionCallbacks.remove(callback);
-    }
-
-    /** Connect to a device for the active user if available. */
-    @VisibleForTesting
-    void connectToActiveUserDevice() {
-        Executors.defaultThreadFactory().newThread(() -> {
-            logd(TAG, "Received request to connect to active user's device.");
-            connectToActiveUserDeviceInternal();
-        }).start();
-    }
-
-    private void connectToActiveUserDeviceInternal() {
-        try {
-            boolean isLockAcquired = mIsConnectingToUserDevice.compareAndSet(false, true);
-            if (!isLockAcquired) {
-                logd(TAG, "A request has already been made to connect to this user's device. "
-                        + "Ignoring redundant request.");
-                return;
-            }
-            List<AssociatedDevice> userDevices = mStorage.getActiveUserAssociatedDevices();
-            if (userDevices.isEmpty()) {
-                logw(TAG, "No devices associated with active user. Ignoring.");
-                mIsConnectingToUserDevice.set(false);
-                return;
-            }
-
-            // Only currently support one device per user for fast association, so take the
-            // first one.
-            AssociatedDevice userDevice = userDevices.get(0);
-            if (!userDevice.isConnectionEnabled()) {
-                logd(TAG, "Connection is disabled on device " + userDevice + ".");
-                mIsConnectingToUserDevice.set(false);
-                return;
-            }
-            if (mConnectedDevices.containsKey(userDevice.getDeviceId())) {
-                logd(TAG, "Device has already been connected. No need to attempt connection "
-                        + "again.");
-                mIsConnectingToUserDevice.set(false);
-                return;
-            }
-            EventLog.onStartDeviceSearchStarted();
-            mCarBluetoothManager.connectToDevice(UUID.fromString(userDevice.getDeviceId()));
-        } catch (Exception e) {
-            loge(TAG, "Exception while attempting connection with active user's device.", e);
-        }
-    }
-
-    /**
-     * Start the association with a new device.
-     *
-     * @param callback Callback for association events.
-     */
-    public void startAssociation(@NonNull AssociationCallback callback) {
-        mAssociationCallback = callback;
-        Executors.defaultThreadFactory().newThread(() -> {
-            logd(TAG, "Received request to start association.");
-            mCarBluetoothManager.startAssociation(getNameForAssociation(),
-                    mInternalAssociationCallback);
-        }).start();
-    }
-
-    /**
-     * Start association with an out of band device.
-     *
-     * @param device   The out of band eligible device.
-     * @param callback Callback for association events.
-     */
-    public void startOutOfBandAssociation(@NonNull OobEligibleDevice device,
-            @NonNull AssociationCallback callback) {
-        logd(TAG, "Received request to start out of band association.");
-        mAssociationCallback = callback;
-        mOobChannel = new BluetoothRfcommChannel();
-        mOobChannel.completeOobDataExchange(device, new OobChannel.Callback() {
-            @Override
-            public void onOobExchangeSuccess() {
-                logd(TAG, "Out of band exchange succeeded. Proceeding to association with device.");
-                Executors.defaultThreadFactory().newThread(() -> {
-                    mCarBluetoothManager.startOutOfBandAssociation(
-                            getNameForAssociation(),
-                            mOobChannel, mInternalAssociationCallback);
-                }).start();
-            }
-
-            @Override
-            public void onOobExchangeFailure() {
-                loge(TAG, "Out of band exchange failed.");
-                mInternalAssociationCallback.onAssociationError(
-                        DEVICE_ERROR_INVALID_ENCRYPTION_KEY);
-                mOobChannel = null;
-                mAssociationCallback = null;
-            }
-        });
-    }
-
-    /** Stop the association with any device. */
-    public void stopAssociation(@NonNull AssociationCallback callback) {
-        if (mAssociationCallback != callback) {
-            logd(TAG, "Stop association called with unrecognized callback. Ignoring.");
-            return;
-        }
-        logd(TAG, "Stopping association.");
-        mAssociationCallback = null;
-        mCarBluetoothManager.stopAssociation();
-        if (mOobChannel != null) {
-            mOobChannel.interrupt();
-        }
-        mOobChannel = null;
-    }
-
-    /**
-     * Get a list of associated devices for the given user.
-     *
-     * @return Associated device list.
-     */
-    @NonNull
-    public List<AssociatedDevice> getActiveUserAssociatedDevices() {
-        return mStorage.getActiveUserAssociatedDevices();
-    }
-
-    /** Notify that the user has accepted a pairing code or any out-of-band confirmation. */
-    public void notifyOutOfBandAccepted() {
-        mCarBluetoothManager.notifyOutOfBandAccepted();
-    }
-
-    /**
-     * Remove the associated device with the given device identifier for the current user.
-     *
-     * @param deviceId Device identifier.
-     */
-    public void removeActiveUserAssociatedDevice(@NonNull String deviceId) {
-        mStorage.removeAssociatedDeviceForActiveUser(deviceId);
-        disconnectDevice(deviceId);
-    }
-
-    /**
-     * Enable connection on an associated device.
-     *
-     * @param deviceId Device identifier.
-     */
-    public void enableAssociatedDeviceConnection(@NonNull String deviceId) {
-        logd(TAG, "enableAssociatedDeviceConnection() called on " + deviceId);
-        mStorage.updateAssociatedDeviceConnectionEnabled(deviceId,
-                /* isConnectionEnabled= */ true);
-
-        connectToActiveUserDevice();
-    }
-
-    /**
-     * Disable connection on an associated device.
-     *
-     * @param deviceId Device identifier.
-     */
-    public void disableAssociatedDeviceConnection(@NonNull String deviceId) {
-        logd(TAG, "disableAssociatedDeviceConnection() called on " + deviceId);
-        mStorage.updateAssociatedDeviceConnectionEnabled(deviceId,
-                /* isConnectionEnabled= */ false);
-        disconnectDevice(deviceId);
-    }
-
-    private void disconnectDevice(String deviceId) {
-        ConnectedDevice device = mConnectedDevices.get(deviceId);
-        if (device != null) {
-            mCarBluetoothManager.disconnectDevice(deviceId);
-            removeConnectedDevice(deviceId);
-        }
-    }
-
-    /**
-     * Register a callback for a specific device and recipient.
-     *
-     * @param device      {@link ConnectedDevice} to register triggers on.
-     * @param recipientId {@link UUID} to register as recipient of.
-     * @param callback    {@link DeviceCallback} to register.
-     * @param executor    {@link Executor} on which to execute callback.
-     */
-    public void registerDeviceCallback(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
-            @NonNull DeviceCallback callback, @NonNull Executor executor) {
-        if (isRecipientBlacklisted(recipientId)) {
-            notifyOfBlacklisting(device, recipientId, callback, executor);
-            return;
-        }
-        logd(TAG, "New callback registered on device " + device.getDeviceId() + " for recipient "
-                + recipientId);
-        String deviceId = device.getDeviceId();
-        Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
-                mDeviceCallbacks.computeIfAbsent(deviceId, key -> new ConcurrentHashMap<>());
-
-        // Device already has a callback registered with this recipient UUID. For the
-        // protection of the user, this UUID is now blacklisted from future subscriptions
-        // and the original subscription is notified and removed.
-        if (recipientCallbacks.containsKey(recipientId)) {
-            blacklistRecipient(deviceId, recipientId);
-            notifyOfBlacklisting(device, recipientId, callback, executor);
-            return;
-        }
-
-        ThreadSafeCallbacks<DeviceCallback> newCallbacks = new ThreadSafeCallbacks<>();
-        newCallbacks.add(callback, executor);
-        recipientCallbacks.put(recipientId, newCallbacks);
-
-        List<byte[]> messages = popMissedMessages(recipientId, device.getDeviceId());
-        if (messages != null) {
-            for (byte[] message : messages) {
-                newCallbacks.invoke(deviceCallback ->
-                        deviceCallback.onMessageReceived(device, message));
-            }
-        }
-    }
-
-    /**
-     * Set the delegate for message delivery operations.
-     *
-     * @param delegate The {@link MessageDeliveryDelegate} to set. {@code null} to unset.
-     */
-    public void setMessageDeliveryDelegate(@Nullable MessageDeliveryDelegate delegate) {
-        mMessageDeliveryDelegate = delegate;
-    }
-
-    private void notifyOfBlacklisting(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
-            @NonNull DeviceCallback callback, @NonNull Executor executor) {
-        loge(TAG, "Multiple callbacks registered for recipient " + recipientId + "! Your "
-                + "recipient id is no longer secure and has been blocked from future use.");
-        executor.execute(() ->
-                callback.onDeviceError(device, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED));
-    }
-
-    private void saveMissedMessage(@NonNull String deviceId, @NonNull UUID recipientId,
-            @NonNull byte[] message) {
-        // Store last message in case recipient registers callbacks in the future.
-        logd(TAG, "No recipient registered for device " + deviceId + " and recipient "
-                + recipientId + " combination. Saving message.");
-        mRecipientMissedMessages.computeIfAbsent(recipientId, __ -> new HashMap<>())
-                .computeIfAbsent(deviceId, __ -> new ArrayList<>()).add(message);
-    }
-
-    /**
-     * Remove the last message sent for this device prior to a {@link DeviceCallback} being
-     * registered.
-     *
-     * @param recipientId Recipient's id
-     * @param deviceId    Device id
-     * @return The missed {@code byte[]} messages, or {@code null} if no messages were
-     * missed.
-     */
-    @Nullable
-    private List<byte[]> popMissedMessages(@NonNull UUID recipientId, @NonNull String deviceId) {
-        Map<String, List<byte[]>> missedMessages = mRecipientMissedMessages.get(recipientId);
-        if (missedMessages == null) {
-            return null;
-        }
-
-        return missedMessages.remove(deviceId);
-    }
-
-    /**
-     * Unregister callback from device events.
-     *
-     * @param device      {@link ConnectedDevice} callback was registered on.
-     * @param recipientId {@link UUID} callback was registered under.
-     * @param callback    {@link DeviceCallback} to unregister.
-     */
-    public void unregisterDeviceCallback(@NonNull ConnectedDevice device,
-            @NonNull UUID recipientId, @NonNull DeviceCallback callback) {
-        logd(TAG, "Device callback unregistered on device " + device.getDeviceId() + " for "
-                + "recipient " + recipientId + ".");
-
-        Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
-                mDeviceCallbacks.get(device.getDeviceId());
-        if (recipientCallbacks == null) {
-            return;
-        }
-        ThreadSafeCallbacks<DeviceCallback> callbacks = recipientCallbacks.get(recipientId);
-        if (callbacks == null) {
-            return;
-        }
-
-        callbacks.remove(callback);
-        if (callbacks.size() == 0) {
-            recipientCallbacks.remove(recipientId);
-        }
-    }
-
-    /**
-     * Securely send message to a device.
-     *
-     * @param device      {@link ConnectedDevice} to send the message to.
-     * @param recipientId Recipient {@link UUID}.
-     * @param message     Message to send.
-     * @throws IllegalStateException Secure channel has not been established.
-     */
-    public void sendMessageSecurely(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
-            @NonNull byte[] message) throws IllegalStateException {
-        sendMessage(device, recipientId, message, /* isEncrypted= */ true);
-    }
-
-    /**
-     * Send an unencrypted message to a device.
-     *
-     * @param device      {@link ConnectedDevice} to send the message to.
-     * @param recipientId Recipient {@link UUID}.
-     * @param message     Message to send.
-     */
-    public void sendMessageUnsecurely(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
-            @NonNull byte[] message) {
-        sendMessage(device, recipientId, message, /* isEncrypted= */ false);
-    }
-
-    private void sendMessage(@NonNull ConnectedDevice device, @NonNull UUID recipientId,
-            @NonNull byte[] message, boolean isEncrypted) throws IllegalStateException {
-        String deviceId = device.getDeviceId();
-        logd(TAG, "Sending new message to device " + deviceId + " for " + recipientId
-                + " containing " + message.length + ". Message will be sent securely: "
-                + isEncrypted + ".");
-
-        ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
-        if (connectedDevice == null) {
-            loge(TAG, "Attempted to send message to unknown device " + deviceId + ". Ignoring.");
-            return;
-        }
-
-        if (isEncrypted && !connectedDevice.hasSecureChannel()) {
-            throw new IllegalStateException("Cannot send a message securely to device that has not "
-                    + "established a secure channel.");
-        }
-
-        mCarBluetoothManager.sendMessage(deviceId, new DeviceMessage(recipientId, isEncrypted,
-                message));
-    }
-
-    private boolean isRecipientBlacklisted(UUID recipientId) {
-        return mBlacklistedRecipients.contains(recipientId);
-    }
-
-    private void blacklistRecipient(@NonNull String deviceId, @NonNull UUID recipientId) {
-        Map<UUID, ThreadSafeCallbacks<DeviceCallback>> recipientCallbacks =
-                mDeviceCallbacks.get(deviceId);
-        if (recipientCallbacks == null) {
-            // Should never happen, but null-safety check.
-            return;
-        }
-
-        ThreadSafeCallbacks<DeviceCallback> existingCallback = recipientCallbacks.get(recipientId);
-        if (existingCallback == null) {
-            // Should never happen, but null-safety check.
-            return;
-        }
-
-        ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
-        if (connectedDevice != null) {
-            recipientCallbacks.get(recipientId).invoke(
-                    callback ->
-                            callback.onDeviceError(connectedDevice,
-                                    DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
-            );
-        }
-
-        recipientCallbacks.remove(recipientId);
-        mBlacklistedRecipients.add(recipientId);
-    }
-
-    @VisibleForTesting
-    void addConnectedDevice(@NonNull String deviceId) {
-        if (mConnectedDevices.containsKey(deviceId)) {
-            // Device already connected. No-op until secure channel established.
-            return;
-        }
-        logd(TAG, "New device with id " + deviceId + " connected.");
-        ConnectedDevice connectedDevice = new ConnectedDevice(
-                deviceId,
-                /* deviceName= */ null,
-                mStorage.getActiveUserAssociatedDeviceIds().contains(deviceId),
-                /* hasSecureChannel= */ false
-        );
-
-        mConnectedDevices.put(deviceId, connectedDevice);
-        invokeConnectionCallbacks(connectedDevice.isAssociatedWithActiveUser(),
-                callback -> callback.onDeviceConnected(connectedDevice));
-    }
-
-    @VisibleForTesting
-    void removeConnectedDevice(@NonNull String deviceId) {
-        logd(TAG, "Device " + deviceId + " disconnected.");
-        ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
-        mIsConnectingToUserDevice.set(false);
-        if (connectedDevice == null) {
-            return;
-        }
-
-        mConnectedDevices.remove(deviceId);
-        boolean isAssociated = connectedDevice.isAssociatedWithActiveUser();
-        invokeConnectionCallbacks(isAssociated,
-                callback -> callback.onDeviceDisconnected(connectedDevice));
-
-        if (isAssociated || mConnectedDevices.isEmpty()) {
-            // Try to regain connection to active user's device.
-            connectToActiveUserDevice();
-        }
-    }
-
-    @VisibleForTesting
-    void onSecureChannelEstablished(@NonNull String deviceId) {
-        if (mConnectedDevices.get(deviceId) == null) {
-            loge(TAG, "Secure channel established on unknown device " + deviceId + ".");
-            return;
-        }
-        ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
-        ConnectedDevice updatedConnectedDevice = new ConnectedDevice(connectedDevice.getDeviceId(),
-                connectedDevice.getDeviceName(), connectedDevice.isAssociatedWithActiveUser(),
-                /* hasSecureChannel= */ true);
-
-        boolean notifyCallbacks = mConnectedDevices.get(deviceId) != null;
-
-        // TODO (b/143088482) Implement interrupt
-        // Ignore if central already holds the active device connection and interrupt the
-        // connection.
-
-        mConnectedDevices.put(deviceId, updatedConnectedDevice);
-        logd(TAG, "Secure channel established to " + deviceId + " . Notifying callbacks: "
-                + notifyCallbacks + ".");
-        if (notifyCallbacks) {
-            notifyAllDeviceCallbacks(deviceId,
-                    callback -> callback.onSecureChannelEstablished(updatedConnectedDevice));
-        }
-    }
-
-    @VisibleForTesting
-    void onMessageReceived(@NonNull String deviceId, @NonNull DeviceMessage message) {
-        logd(TAG, "New message received from device " + deviceId + " intended for "
-                + message.getRecipient() + " containing " + message.getMessage().length
-                + " bytes.");
-
-        ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
-        if (connectedDevice == null) {
-            logw(TAG, "Received message from unknown device " + deviceId + "or to unknown "
-                    + "recipient " + message.getRecipient() + ".");
-            return;
-        }
-
-        if (mMessageDeliveryDelegate != null
-                && !mMessageDeliveryDelegate.shouldDeliverMessageForDevice(connectedDevice)) {
-            logw(TAG, "The message delegate has rejected this message. It will not be "
-                    + "delivered to the intended recipient.");
-            return;
-        }
-
-        UUID recipientId = message.getRecipient();
-        Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
-                mDeviceCallbacks.get(deviceId);
-        if (deviceCallbacks == null) {
-            saveMissedMessage(deviceId, recipientId, message.getMessage());
-            return;
-        }
-        ThreadSafeCallbacks<DeviceCallback> recipientCallbacks =
-                deviceCallbacks.get(recipientId);
-        if (recipientCallbacks == null) {
-            saveMissedMessage(deviceId, recipientId, message.getMessage());
-            return;
-        }
-
-        recipientCallbacks.invoke(
-                callback -> callback.onMessageReceived(connectedDevice, message.getMessage()));
-    }
-
-    @VisibleForTesting
-    void deviceErrorOccurred(@NonNull String deviceId) {
-        ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
-        if (connectedDevice == null) {
-            logw(TAG, "Failed to establish secure channel on unknown device " + deviceId + ".");
-            return;
-        }
-
-        notifyAllDeviceCallbacks(deviceId, callback -> callback.onDeviceError(connectedDevice,
-                        DEVICE_ERROR_INVALID_SECURITY_KEY));
-    }
-
-    @VisibleForTesting
-    void onAssociationCompleted(@NonNull String deviceId) {
-        ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
-        if (connectedDevice == null) {
-            return;
-        }
-
-        // The previous device is now obsolete and should be replaced with a new one properly
-        // reflecting the state of belonging to the active user and notify features.
-        if (connectedDevice.isAssociatedWithActiveUser()) {
-            // Device was already marked as belonging to active user. No need to reissue callbacks.
-            return;
-        }
-        removeConnectedDevice(deviceId);
-        addConnectedDevice(deviceId);
-    }
-
-    @NonNull
-    private List<String> getActiveUserDeviceIds() {
-        return mStorage.getActiveUserAssociatedDeviceIds();
-    }
-
-    private void invokeConnectionCallbacks(boolean belongsToActiveUser,
-            @NonNull Consumer<ConnectionCallback> notification) {
-        logd(TAG, "Notifying connection callbacks for device belonging to active user "
-                + belongsToActiveUser + ".");
-        if (belongsToActiveUser) {
-            mActiveUserConnectionCallbacks.invoke(notification);
-        }
-        mAllUserConnectionCallbacks.invoke(notification);
-    }
-
-    private void notifyAllDeviceCallbacks(@NonNull String deviceId,
-            @NonNull Consumer<DeviceCallback> notification) {
-        logd(TAG, "Notifying all device callbacks for device " + deviceId + ".");
-        Map<UUID, ThreadSafeCallbacks<DeviceCallback>> deviceCallbacks =
-                mDeviceCallbacks.get(deviceId);
-        if (deviceCallbacks == null) {
-            return;
-        }
-
-        for (ThreadSafeCallbacks<DeviceCallback> callbacks : deviceCallbacks.values()) {
-            callbacks.invoke(notification);
-        }
-    }
-
-    /**
-     * Returns the name that should be used for the device during enrollment of a trusted device.
-     *
-     * <p>The returned name will be a combination of a prefix sysprop and randomized digits.
-     */
-    @NonNull
-    private String getNameForAssociation() {
-        if (mNameForAssociation == null) {
-            mNameForAssociation = ByteUtils.generateRandomNumberString(DEVICE_NAME_LENGTH_LIMIT);
-        }
-        return mNameForAssociation;
-    }
-
-    @NonNull
-    private CarBluetoothManager.Callback generateCarBleCallback(
-            @NonNull CarBluetoothManager carBluetoothManager) {
-        return new CarBluetoothManager.Callback() {
-            @Override
-            public void onDeviceConnected(String deviceId) {
-                EventLog.onDeviceIdReceived();
-                addConnectedDevice(deviceId);
-            }
-
-            @Override
-            public void onDeviceDisconnected(String deviceId) {
-                removeConnectedDevice(deviceId);
-            }
-
-            @Override
-            public void onSecureChannelEstablished(String deviceId) {
-                EventLog.onSecureChannelEstablished();
-                ConnectedDeviceManager.this.onSecureChannelEstablished(deviceId);
-            }
-
-            @Override
-            public void onMessageReceived(String deviceId, DeviceMessage message) {
-                ConnectedDeviceManager.this.onMessageReceived(deviceId, message);
-            }
-
-            @Override
-            public void onSecureChannelError(String deviceId) {
-                deviceErrorOccurred(deviceId);
-            }
-        };
-    }
-
-    private final AssociationCallback mInternalAssociationCallback = new AssociationCallback() {
-        @Override
-        public void onAssociationStartSuccess(String deviceName) {
-            if (mAssociationCallback != null) {
-                mAssociationCallback.onAssociationStartSuccess(deviceName);
-            }
-        }
-
-        @Override
-        public void onAssociationStartFailure() {
-            if (mAssociationCallback != null) {
-                mAssociationCallback.onAssociationStartFailure();
-            }
-        }
-
-        @Override
-        public void onAssociationError(int error) {
-            if (mAssociationCallback != null) {
-                mAssociationCallback.onAssociationError(error);
-            }
-        }
-
-        @Override
-        public void onVerificationCodeAvailable(String code) {
-            if (mAssociationCallback != null) {
-                mAssociationCallback.onVerificationCodeAvailable(code);
-            }
-        }
-
-        @Override
-        public void onAssociationCompleted(String deviceId) {
-            if (mAssociationCallback != null) {
-                mAssociationCallback.onAssociationCompleted(deviceId);
-            }
-            ConnectedDeviceManager.this.onAssociationCompleted(deviceId);
-        }
-    };
-
-    private final AssociatedDeviceCallback mAssociatedDeviceCallback =
-            new AssociatedDeviceCallback() {
-                @Override
-                public void onAssociatedDeviceAdded(AssociatedDevice device) {
-                    mDeviceAssociationCallbacks.invoke(callback ->
-                            callback.onAssociatedDeviceAdded(device));
-                }
-
-                @Override
-                public void onAssociatedDeviceRemoved(AssociatedDevice device) {
-                    mDeviceAssociationCallbacks.invoke(callback ->
-                            callback.onAssociatedDeviceRemoved(device));
-                    logd(TAG, "Successfully removed associated device " + device + ".");
-                }
-
-                @Override
-                public void onAssociatedDeviceUpdated(AssociatedDevice device) {
-                    mDeviceAssociationCallbacks.invoke(callback ->
-                            callback.onAssociatedDeviceUpdated(device));
-                }
-            };
-
-    /** Callback for triggered connection events from {@link ConnectedDeviceManager}. */
-    public interface ConnectionCallback {
-        /** Triggered when a new device has connected. */
-        void onDeviceConnected(@NonNull ConnectedDevice device);
-
-        /** Triggered when a device has disconnected. */
-        void onDeviceDisconnected(@NonNull ConnectedDevice device);
-    }
-
-    /** Triggered device events for a connected device from {@link ConnectedDeviceManager}. */
-    public interface DeviceCallback {
-        /**
-         * Triggered when secure channel has been established on a device. Encrypted messaging now
-         * available.
-         */
-        void onSecureChannelEstablished(@NonNull ConnectedDevice device);
-
-        /** Triggered when a new message is received from a device. */
-        void onMessageReceived(@NonNull ConnectedDevice device, @NonNull byte[] message);
-
-        /** Triggered when an error has occurred for a device. */
-        void onDeviceError(@NonNull ConnectedDevice device, @DeviceError int error);
-    }
-
-    /** Callback for association device related events. */
-    public interface DeviceAssociationCallback {
-
-        /** Triggered when an associated device has been added. */
-        void onAssociatedDeviceAdded(@NonNull AssociatedDevice device);
-
-        /** Triggered when an associated device has been removed. */
-        void onAssociatedDeviceRemoved(@NonNull AssociatedDevice device);
-
-        /** Triggered when the name of an associated device has been updated. */
-        void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device);
-    }
-
-    /** Delegate for message delivery operations. */
-    public interface MessageDeliveryDelegate {
-
-        /** Indicate whether a message should be delivered for the specified device. */
-        boolean shouldDeliverMessageForDevice(@NonNull ConnectedDevice device);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/AssociationSecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/AssociationSecureChannel.java
deleted file mode 100644
index 65a05da..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/AssociationSecureChannel.java
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static android.car.encryptionrunner.EncryptionRunnerFactory.EncryptionRunnerType;
-import static android.car.encryptionrunner.EncryptionRunnerFactory.newRunner;
-import static android.car.encryptionrunner.HandshakeMessage.HandshakeState;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.HandshakeMessage;
-import android.car.encryptionrunner.Key;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import java.security.InvalidParameterException;
-import java.util.Arrays;
-import java.util.UUID;
-
-/**
- * A secure channel established with the association flow.
- */
-public class AssociationSecureChannel extends SecureChannel {
-
-    private static final String TAG = "AssociationSecureChannel";
-
-    private static final int DEVICE_ID_BYTES = 16;
-
-    private final ConnectedDeviceStorage mStorage;
-
-    private ShowVerificationCodeListener mShowVerificationCodeListener;
-
-    @HandshakeState
-    private int mState = HandshakeState.UNKNOWN;
-
-    private Key mPendingKey;
-
-    private String mDeviceId;
-
-    public AssociationSecureChannel(DeviceMessageStream stream, ConnectedDeviceStorage storage) {
-        this(stream, storage, newRunner(EncryptionRunnerType.UKEY2));
-    }
-
-    AssociationSecureChannel(DeviceMessageStream stream, ConnectedDeviceStorage storage,
-            EncryptionRunner encryptionRunner) {
-        super(stream, encryptionRunner);
-        encryptionRunner.setIsReconnect(false);
-        mStorage = storage;
-    }
-
-    @Override
-    void processHandshake(@NonNull byte[] message) throws HandshakeException {
-        switch (mState) {
-            case HandshakeState.UNKNOWN:
-                processHandshakeUnknown(message);
-                break;
-            case HandshakeState.IN_PROGRESS:
-                processHandshakeInProgress(message);
-                break;
-            case HandshakeState.FINISHED:
-                processHandshakeDeviceIdAndSecret(message);
-                break;
-            default:
-                loge(TAG, "Encountered unexpected handshake state: " + mState + ".");
-                notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-        }
-    }
-
-    private void processHandshakeUnknown(@NonNull byte[] message) throws HandshakeException {
-        logd(TAG, "Responding to handshake init request.");
-        HandshakeMessage handshakeMessage = getEncryptionRunner().respondToInitRequest(message);
-        mState = handshakeMessage.getHandshakeState();
-        sendHandshakeMessage(handshakeMessage.getNextMessage(), /* isEncrypted= */ false);
-    }
-
-    private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
-        logd(TAG, "Continuing handshake.");
-        HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
-        mState = handshakeMessage.getHandshakeState();
-        if (mState != HandshakeState.VERIFICATION_NEEDED) {
-            loge(TAG, "processHandshakeInProgress: Encountered unexpected handshake state: "
-                    + mState + ".");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-            return;
-        }
-
-        String code = handshakeMessage.getVerificationCode();
-        if (code == null) {
-            loge(TAG, "Unable to get verification code.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
-            return;
-        }
-        processVerificationCode(code);
-    }
-
-    private void processVerificationCode(@NonNull String code) {
-        if (mShowVerificationCodeListener == null) {
-            loge(TAG,
-                    "No verification code listener has been set. Unable to display "
-                            + "verification "
-                            + "code to user.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-            return;
-        }
-
-        logd(TAG, "Showing pairing code: " + code);
-        mShowVerificationCodeListener.showVerificationCode(code);
-    }
-
-    private void processHandshakeDeviceIdAndSecret(@NonNull byte[] message) {
-        UUID deviceId = ByteUtils.bytesToUUID(Arrays.copyOf(message, DEVICE_ID_BYTES));
-        if (deviceId == null) {
-            loge(TAG, "Received invalid device id. Aborting.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
-            return;
-        }
-        mDeviceId = deviceId.toString();
-        notifyCallback(callback -> callback.onDeviceIdReceived(mDeviceId));
-
-        mStorage.saveEncryptionKey(mDeviceId, mPendingKey.asBytes());
-        mPendingKey = null;
-        try {
-            mStorage.saveChallengeSecret(mDeviceId,
-                    Arrays.copyOfRange(message, DEVICE_ID_BYTES, message.length));
-        } catch (InvalidParameterException e) {
-            loge(TAG, "Error saving challenge secret.", e);
-            notifySecureChannelFailure(CHANNEL_ERROR_STORAGE_ERROR);
-            return;
-        }
-
-        notifyCallback(Callback::onSecureChannelEstablished);
-    }
-
-    /** Set the listener that notifies to show verification code. {@code null} to clear. */
-    public void setShowVerificationCodeListener(@Nullable ShowVerificationCodeListener listener) {
-        mShowVerificationCodeListener = listener;
-    }
-
-    @VisibleForTesting
-    @Nullable
-    public ShowVerificationCodeListener getShowVerificationCodeListener() {
-        return mShowVerificationCodeListener;
-    }
-
-    /**
-     * Called by the client to notify that the user has accepted a pairing code or any out-of-band
-     * confirmation, and send confirmation signals to remote bluetooth device.
-     */
-    public void notifyOutOfBandAccepted() {
-        HandshakeMessage message;
-        try {
-            message = getEncryptionRunner().verifyPin();
-        } catch (HandshakeException e) {
-            loge(TAG, "Error during PIN verification", e);
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
-            return;
-        }
-        if (message.getHandshakeState() != HandshakeState.FINISHED) {
-            loge(TAG, "Handshake not finished after calling verify PIN. Instead got "
-                    + "state: " + message.getHandshakeState() + ".");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-            return;
-        }
-
-        Key localKey = message.getKey();
-        if (localKey == null) {
-            loge(TAG, "Unable to finish association, generated key is null.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
-            return;
-        }
-        mState = message.getHandshakeState();
-        setEncryptionKey(localKey);
-        mPendingKey = localKey;
-        logd(TAG, "Pairing code successfully verified.");
-        sendUniqueIdToClient();
-    }
-
-    private void sendUniqueIdToClient() {
-        UUID uniqueId = mStorage.getUniqueId();
-        DeviceMessage deviceMessage = new DeviceMessage(/* recipient= */ null,
-                /* isMessageEncrypted= */ true, ByteUtils.uuidToBytes(uniqueId));
-        logd(TAG, "Sending car's device id of " + uniqueId + " to device.");
-        sendHandshakeMessage(ByteUtils.uuidToBytes(uniqueId), /* isEncrypted= */ true);
-    }
-
-    @HandshakeState
-    int getState() {
-        return mState;
-    }
-
-    void setState(@HandshakeState int state) {
-        mState = state;
-    }
-
-    /** Listener that will be invoked to display verification code. */
-    public interface ShowVerificationCodeListener {
-        /**
-         * Invoke when a verification need to be displayed during device association.
-         *
-         * @param code The verification code to show.
-         */
-        void showVerificationCode(@NonNull String code);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/CarBluetoothManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/CarBluetoothManager.java
deleted file mode 100644
index 9ca699b..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/CarBluetoothManager.java
+++ /dev/null
@@ -1,485 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_INVALID_HANDSHAKE;
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ThreadSafeCallbacks;
-
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.Executor;
-
-/**
- * Generic BLE manager for a car that keeps track of connected devices and their associated
- * callbacks.
- */
-public abstract class CarBluetoothManager {
-
-    private static final String TAG = "CarBluetoothManager";
-
-    protected final ConnectedDeviceStorage mStorage;
-
-    protected final CopyOnWriteArraySet<ConnectedRemoteDevice> mConnectedDevices =
-            new CopyOnWriteArraySet<>();
-
-    protected final ThreadSafeCallbacks<Callback> mCallbacks = new ThreadSafeCallbacks<>();
-
-    private String mClientDeviceName;
-
-    private String mClientDeviceAddress;
-
-    protected CarBluetoothManager(@NonNull ConnectedDeviceStorage connectedDeviceStorage) {
-        mStorage = connectedDeviceStorage;
-    }
-
-    /** Attempt to connect to device with provided id. */
-    public void connectToDevice(@NonNull UUID deviceId) {
-        for (ConnectedRemoteDevice device : mConnectedDevices) {
-            if (UUID.fromString(device.mDeviceId).equals(deviceId)) {
-                logd(TAG, "Already connected to device " + deviceId + ".");
-                // Already connected to this device. Ignore requests to connect again.
-                return;
-            }
-        }
-        // Clear any previous session before starting a new one.
-        reset();
-        initiateConnectionToDevice(deviceId);
-    }
-
-    /** Start to connect to associated devices */
-    public abstract void initiateConnectionToDevice(@NonNull UUID deviceId);
-
-    /** Start the association with a new device */
-    public abstract void startAssociation(@NonNull String nameForAssociation,
-            @NonNull AssociationCallback callback);
-
-    /** Start the association with a new device using out of band verification code exchange */
-    public abstract void startOutOfBandAssociation(
-            @NonNull String nameForAssociation,
-            @NonNull OobChannel oobChannel,
-            @NonNull AssociationCallback callback);
-
-    /** Disconnect the provided device from this manager. */
-    public abstract void disconnectDevice(@NonNull String deviceId);
-
-    /** Get current {@link AssociationCallback}. */
-    @Nullable
-    public abstract AssociationCallback getAssociationCallback();
-
-    /** Set current {@link AssociationCallback}. */
-    public abstract void setAssociationCallback(@Nullable AssociationCallback callback);
-
-    /** Set the value of the client device name */
-    public void setClientDeviceName(String deviceName) {
-        mClientDeviceName = deviceName;
-    }
-
-    /** Set the value of client device's mac address */
-    public void setClientDeviceAddress(String macAddress) {
-        mClientDeviceAddress = macAddress;
-    }
-
-    /**
-     * Initialize and start the manager.
-     */
-    @CallSuper
-    public void start() {}
-
-    /**
-     * Stop the manager and clean up.
-     */
-    public void stop() {
-        for (ConnectedRemoteDevice device : mConnectedDevices) {
-            if (device.mGatt != null) {
-                device.mGatt.close();
-            }
-        }
-        mConnectedDevices.clear();
-    }
-
-    /**
-     * Stop the association process with any device.
-     */
-    public void stopAssociation() {
-        if (!isAssociating()) {
-            return;
-        }
-        reset();
-    }
-
-    /**
-     * Register a {@link Callback} to be notified on the {@link Executor}.
-     */
-    public void registerCallback(@NonNull Callback callback, @NonNull Executor executor) {
-        mCallbacks.add(callback, executor);
-    }
-
-    /**
-     * Unregister a callback.
-     *
-     * @param callback The {@link Callback} to unregister.
-     */
-    public void unregisterCallback(@NonNull Callback callback) {
-        mCallbacks.remove(callback);
-    }
-
-    /**
-     * Send a message to a connected device.
-     *
-     * @param deviceId Id of connected device.
-     * @param message  {@link DeviceMessage} to send.
-     */
-    public void sendMessage(@NonNull String deviceId, @NonNull DeviceMessage message) {
-        ConnectedRemoteDevice device = getConnectedDevice(deviceId);
-        if (device == null) {
-            logw(TAG, "Attempted to send message to unknown device $deviceId. Ignored.");
-            return;
-        }
-
-        sendMessage(device, message);
-    }
-
-    /**
-     * Send a message to a connected device.
-     *
-     * @param device  The connected {@link ConnectedRemoteDevice}.
-     * @param message {@link DeviceMessage} to send.
-     */
-    public void sendMessage(@NonNull ConnectedRemoteDevice device, @NonNull DeviceMessage message) {
-        String deviceId = device.mDeviceId;
-        if (deviceId == null) {
-            deviceId = "Unidentified device";
-        }
-
-        logd(TAG, "Writing " + message.getMessage().length + " bytes to " + deviceId + ".");
-        device.mSecureChannel.sendClientMessage(message);
-    }
-
-    /** Clean manager status and callbacks. */
-    @CallSuper
-    public void reset() {
-        mClientDeviceAddress = null;
-        mClientDeviceName = null;
-        mConnectedDevices.clear();
-    }
-
-    /** Notify that the user has accepted a pairing code or other out-of-band confirmation. */
-    public void notifyOutOfBandAccepted() {
-        if (getConnectedDevice() == null) {
-            disconnectWithError("Null connected device found when out-of-band confirmation "
-                    + "received.");
-            return;
-        }
-
-        AssociationSecureChannel secureChannel =
-                (AssociationSecureChannel) getConnectedDevice().mSecureChannel;
-        if (secureChannel == null) {
-            disconnectWithError("Null SecureBleChannel found for the current connected device "
-                    + "when out-of-band confirmation received.");
-            return;
-        }
-
-        secureChannel.notifyOutOfBandAccepted();
-    }
-
-    /** Returns the secure channel of current connected device. */
-    @Nullable
-    public SecureChannel getConnectedDeviceChannel() {
-        ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-        if (connectedDevice == null) {
-            return null;
-        }
-
-        return connectedDevice.mSecureChannel;
-    }
-
-    /**
-     * Get the {@link ConnectedRemoteDevice} with matching {@link BluetoothGatt} if available.
-     * Returns
-     * {@code null} if no matches are found.
-     */
-    @Nullable
-    protected final ConnectedRemoteDevice getConnectedDevice(@NonNull BluetoothGatt gatt) {
-        for (ConnectedRemoteDevice device : mConnectedDevices) {
-            if (device.mGatt == gatt) {
-                return device;
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * Get the {@link ConnectedRemoteDevice} with matching {@link BluetoothDevice} if available.
-     * Returns
-     * {@code null} if no matches are found.
-     */
-    @Nullable
-    protected final ConnectedRemoteDevice getConnectedDevice(@NonNull BluetoothDevice device) {
-        for (ConnectedRemoteDevice connectedDevice : mConnectedDevices) {
-            if (device.equals(connectedDevice.mDevice)) {
-                return connectedDevice;
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * Get the {@link ConnectedRemoteDevice} with matching device id if available. Returns {@code
-     * null} if no matches are found.
-     */
-    @Nullable
-    protected final ConnectedRemoteDevice getConnectedDevice(@NonNull String deviceId) {
-        for (ConnectedRemoteDevice device : mConnectedDevices) {
-            if (deviceId.equals(device.mDeviceId)) {
-                return device;
-            }
-        }
-
-        return null;
-    }
-
-    /** Add the {@link ConnectedRemoteDevice} that has connected. */
-    protected final void addConnectedDevice(@NonNull ConnectedRemoteDevice device) {
-        mConnectedDevices.add(device);
-    }
-
-    /** Return the number of devices currently connected. */
-    protected final int getConnectedDevicesCount() {
-        return mConnectedDevices.size();
-    }
-
-    /** Remove [@link BleDevice} that has been disconnected. */
-    protected final void removeConnectedDevice(@NonNull ConnectedRemoteDevice device) {
-        mConnectedDevices.remove(device);
-    }
-
-    /** Return [@code true} if the manager is currently in an association process. */
-    protected final boolean isAssociating() {
-        return getAssociationCallback() != null;
-    }
-
-    /**
-     * Set the device id of {@link ConnectedRemoteDevice} and then notify device connected callback.
-     *
-     * @param deviceId The device id received from remote device.
-     */
-    protected final void setDeviceIdAndNotifyCallbacks(@NonNull String deviceId) {
-        logd(TAG, "Setting device id: " + deviceId);
-        ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-        if (connectedDevice == null) {
-            disconnectWithError("Null connected device found when device id received.");
-            return;
-        }
-
-        connectedDevice.mDeviceId = deviceId;
-        mCallbacks.invoke(callback -> callback.onDeviceConnected(deviceId));
-    }
-
-    /** Log error which causes the disconnect with {@link Exception} and notify callbacks. */
-    protected final void disconnectWithError(@NonNull String errorMessage, @Nullable Exception e) {
-        loge(TAG, errorMessage, e);
-        if (isAssociating()) {
-            getAssociationCallback().onAssociationError(DEVICE_ERROR_INVALID_HANDSHAKE);
-        }
-        reset();
-    }
-
-    /** Log error which cause the disconnection and notify callbacks. */
-    protected final void disconnectWithError(@NonNull String errorMessage) {
-        disconnectWithError(errorMessage, null);
-    }
-
-    /** Return the current connected device. */
-    @Nullable
-    protected final ConnectedRemoteDevice getConnectedDevice() {
-        if (mConnectedDevices.isEmpty()) {
-            return null;
-        }
-        // Directly return the next because there will only be one device connected at one time.
-        return mConnectedDevices.iterator().next();
-    }
-
-    protected final SecureChannel.Callback mSecureChannelCallback =
-            new SecureChannel.Callback() {
-                @Override
-                public void onSecureChannelEstablished() {
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-                    if (connectedDevice == null || connectedDevice.mDeviceId == null) {
-                        disconnectWithError("Null device id found when secure channel "
-                                + "established.");
-                        return;
-                    }
-                    String deviceId = connectedDevice.mDeviceId;
-                    if (mClientDeviceAddress == null) {
-                        disconnectWithError("Null device address found when secure channel "
-                                + "established.");
-                        return;
-                    }
-
-                    if (isAssociating()) {
-                        logd(TAG, "Secure channel established for un-associated device. Saving "
-                                + "association of that device for current user.");
-                        mStorage.addAssociatedDeviceForActiveUser(
-                                new AssociatedDevice(deviceId, mClientDeviceAddress,
-                                        mClientDeviceName, /* isConnectionEnabled= */ true));
-                        AssociationCallback callback = getAssociationCallback();
-                        if (callback != null) {
-                            callback.onAssociationCompleted(deviceId);
-                            setAssociationCallback(null);
-                        }
-                    }
-                    mCallbacks.invoke(callback -> callback.onSecureChannelEstablished(deviceId));
-                }
-
-                @Override
-                public void onEstablishSecureChannelFailure(int error) {
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-                    if (connectedDevice == null || connectedDevice.mDeviceId == null) {
-                        disconnectWithError("Null device id found when secure channel "
-                                + "failed to establish.");
-                        return;
-                    }
-                    String deviceId = connectedDevice.mDeviceId;
-                    mCallbacks.invoke(callback -> callback.onSecureChannelError(deviceId));
-
-                    if (isAssociating()) {
-                        getAssociationCallback().onAssociationError(error);
-                    }
-
-                    disconnectWithError("Error while establishing secure connection.");
-                }
-
-                @Override
-                public void onMessageReceived(DeviceMessage deviceMessage) {
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-                    if (connectedDevice == null || connectedDevice.mDeviceId == null) {
-                        disconnectWithError("Null device id found when message received.");
-                        return;
-                    }
-
-                    logd(TAG, "Received new message from " + connectedDevice.mDeviceId
-                            + " with " + deviceMessage.getMessage().length + " bytes in its "
-                            + "payload. Notifying " + mCallbacks.size() + " callbacks.");
-                    mCallbacks.invoke(
-                            callback -> callback.onMessageReceived(connectedDevice.mDeviceId,
-                                    deviceMessage));
-                }
-
-                @Override
-                public void onMessageReceivedError(Exception exception) {
-                    // TODO(b/143879960) Extend the message error from here to continue up the
-                    // chain.
-                    disconnectWithError("Error while receiving message.");
-                }
-
-                @Override
-                public void onDeviceIdReceived(String deviceId) {
-                    setDeviceIdAndNotifyCallbacks(deviceId);
-                }
-            };
-
-
-    /** State for a connected device. */
-    public enum ConnectedDeviceState {
-        CONNECTING,
-        PENDING_VERIFICATION,
-        CONNECTED,
-        UNKNOWN
-    }
-
-    /**
-     * Container class to hold information about a connected device.
-     */
-    public static class ConnectedRemoteDevice {
-        @NonNull
-        public BluetoothDevice mDevice;
-        @Nullable
-        public BluetoothGatt mGatt;
-        @NonNull
-        public ConnectedDeviceState mState;
-        @Nullable
-        public String mDeviceId;
-        @Nullable
-        public SecureChannel mSecureChannel;
-
-        public ConnectedRemoteDevice(@NonNull BluetoothDevice device,
-                @Nullable BluetoothGatt gatt) {
-            mDevice = device;
-            mGatt = gatt;
-            mState = ConnectedDeviceState.UNKNOWN;
-        }
-    }
-
-    /**
-     * Callback for triggered events from {@link CarBluetoothManager}.
-     */
-    public interface Callback {
-        /**
-         * Triggered when device is connected and device id retrieved. Device is now ready to
-         * receive messages.
-         *
-         * @param deviceId Id of device that has connected.
-         */
-        void onDeviceConnected(@NonNull String deviceId);
-
-        /**
-         * Triggered when device is disconnected.
-         *
-         * @param deviceId Id of device that has disconnected.
-         */
-        void onDeviceDisconnected(@NonNull String deviceId);
-
-        /**
-         * Triggered when device has established encryption for secure communication.
-         *
-         * @param deviceId Id of device that has established encryption.
-         */
-        void onSecureChannelEstablished(@NonNull String deviceId);
-
-        /**
-         * Triggered when a new message is received.
-         *
-         * @param deviceId Id of the device that sent the message.
-         * @param message  {@link DeviceMessage} received.
-         */
-        void onMessageReceived(@NonNull String deviceId, @NonNull DeviceMessage message);
-
-        /**
-         * Triggered when an error when establishing the secure channel.
-         *
-         * @param deviceId Id of the device that experienced the error.
-         */
-        void onSecureChannelError(@NonNull String deviceId);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessage.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessage.java
deleted file mode 100644
index 128decb..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessage.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.StreamProtos.DeviceMessageProto.Message;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.UUID;
-
-/** Holds the needed data from a {@link Message}. */
-public class DeviceMessage {
-
-    private static final String TAG = "DeviceMessage";
-
-    private final UUID mRecipient;
-
-    private final boolean mIsMessageEncrypted;
-
-    private byte[] mMessage;
-
-    public DeviceMessage(@Nullable UUID recipient, boolean isMessageEncrypted,
-            @NonNull byte[] message) {
-        mRecipient = recipient;
-        mIsMessageEncrypted = isMessageEncrypted;
-        mMessage = message;
-    }
-
-    /** Returns the recipient for this message. {@code null} if no recipient set. */
-    @Nullable
-    public UUID getRecipient() {
-        return mRecipient;
-    }
-
-    /** Returns whether this message is encrypted. */
-    public boolean isMessageEncrypted() {
-        return mIsMessageEncrypted;
-    }
-
-    /** Returns the message payload. */
-    @Nullable
-    public byte[] getMessage() {
-        return mMessage;
-    }
-
-    /** Set the message payload. */
-    public void setMessage(@NonNull byte[] message) {
-        mMessage = message;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == this) {
-            return true;
-        }
-        if (!(obj instanceof DeviceMessage)) {
-            return false;
-        }
-        DeviceMessage deviceMessage = (DeviceMessage) obj;
-        return Objects.equals(mRecipient, deviceMessage.mRecipient)
-                && mIsMessageEncrypted == deviceMessage.mIsMessageEncrypted
-                && Arrays.equals(mMessage, deviceMessage.mMessage);
-    }
-
-    @Override
-    public int hashCode() {
-        return 31 * Objects.hash(mRecipient, mIsMessageEncrypted)
-                + Arrays.hashCode(mMessage);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessageStream.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessageStream.java
deleted file mode 100644
index c23618a..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/DeviceMessageStream.java
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.StreamProtos.DeviceMessageProto.Message;
-import com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-import com.android.car.connecteddevice.StreamProtos.PacketProto.Packet;
-import com.android.car.connecteddevice.StreamProtos.VersionExchangeProto.VersionExchange;
-import com.android.car.connecteddevice.util.ByteUtils;
-import com.android.car.protobuf.ByteString;
-import com.android.car.protobuf.InvalidProtocolBufferException;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.ArrayDeque;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Abstract class which includes common logic of different types of {@link DeviceMessageStream}.
- */
-public abstract class DeviceMessageStream {
-
-    private static final String TAG = "DeviceMessageStream";
-
-    // Only version 2 of the messaging and version 2 of the security supported.
-    @VisibleForTesting
-    public static final int MESSAGING_VERSION = 2;
-    @VisibleForTesting
-    public static final int SECURITY_VERSION = 2;
-
-    private final ArrayDeque<Packet> mPacketQueue = new ArrayDeque<>();
-
-    private final Map<Integer, ByteArrayOutputStream> mPendingData = new HashMap<>();
-
-    // messageId -> nextExpectedPacketNumber
-    private final Map<Integer, Integer> mPendingPacketNumber = new HashMap<>();
-
-    private final MessageIdGenerator mMessageIdGenerator = new MessageIdGenerator();
-
-    private final AtomicBoolean mIsVersionExchanged = new AtomicBoolean(false);
-
-    private final AtomicBoolean mIsSendingInProgress = new AtomicBoolean(false);
-
-    private int mMaxWriteSize;
-
-    /** Listener which will be notified when there is new {@link DeviceMessage} received. */
-    private MessageReceivedListener mMessageReceivedListener;
-
-    /** Listener which will be notified when there is error parsing the received message. */
-    private MessageReceivedErrorListener mMessageReceivedErrorListener;
-
-    public DeviceMessageStream(int defaultMaxWriteSize) {
-        mMaxWriteSize = defaultMaxWriteSize;
-    }
-
-    /**
-     * Send data to the connected device. Note: {@link #sendCompleted()} must be called when the
-     * bytes have successfully been sent to indicate the stream is ready to send more data.
-     */
-    protected abstract void send(byte[] data);
-
-    /**
-     * Set the given listener to be notified when a new message was received from the client. If
-     * listener is {@code null}, clear.
-     */
-    public final void setMessageReceivedListener(@Nullable MessageReceivedListener listener) {
-        mMessageReceivedListener = listener;
-    }
-
-    /**
-     * Set the given listener to be notified when there was an error during receiving message from
-     * the client. If listener is {@code null}, clear.
-     */
-    public final void setMessageReceivedErrorListener(
-            @Nullable MessageReceivedErrorListener listener) {
-        mMessageReceivedErrorListener = listener;
-    }
-
-    /**
-     * Notify the {@code mMessageReceivedListener} about the message received if it is  not {@code
-     * null}.
-     *
-     * @param deviceMessage The message received.
-     * @param operationType The operation type of the message.
-     */
-    protected final void notifyMessageReceivedListener(@NonNull DeviceMessage deviceMessage,
-            OperationType operationType) {
-        if (mMessageReceivedListener != null) {
-            mMessageReceivedListener.onMessageReceived(deviceMessage, operationType);
-        }
-    }
-
-    /**
-     * Notify the {@code mMessageReceivedErrorListener} about the message received if it is  not
-     * {@code null}.
-     *
-     * @param e The exception happened when parsing the received message.
-     */
-    protected final void notifyMessageReceivedErrorListener(Exception e) {
-        if (mMessageReceivedErrorListener != null) {
-            mMessageReceivedErrorListener.onMessageReceivedError(e);
-        }
-    }
-
-    /**
-     * Writes the given message to the write characteristic of this stream with operation type
-     * {@code CLIENT_MESSAGE}.
-     *
-     * This method will handle the chunking of messages based on the max write size.
-     *
-     * @param deviceMessage The data object contains recipient, isPayloadEncrypted and message.
-     */
-    public final void writeMessage(@NonNull DeviceMessage deviceMessage) {
-        writeMessage(deviceMessage, OperationType.CLIENT_MESSAGE);
-    }
-
-    /**
-     * Send {@link DeviceMessage} to remote connected devices.
-     *
-     * @param deviceMessage The message which need to be sent
-     * @param operationType The operation type of current message
-     */
-    public final void writeMessage(@NonNull DeviceMessage deviceMessage,
-            OperationType operationType) {
-        Message.Builder builder = Message.newBuilder()
-                .setOperation(operationType)
-                .setIsPayloadEncrypted(deviceMessage.isMessageEncrypted())
-                .setPayload(ByteString.copyFrom(deviceMessage.getMessage()));
-
-        UUID recipient = deviceMessage.getRecipient();
-        if (recipient != null) {
-            builder.setRecipient(ByteString.copyFrom(ByteUtils.uuidToBytes(recipient)));
-        }
-
-        Message message = builder.build();
-        byte[] rawBytes = message.toByteArray();
-        List<Packet> packets;
-        try {
-            packets = PacketFactory.makePackets(rawBytes, mMessageIdGenerator.next(),
-                    mMaxWriteSize);
-        } catch (PacketFactoryException e) {
-            loge(TAG, "Error while creating message packets.", e);
-            return;
-        }
-        mPacketQueue.addAll(packets);
-        writeNextMessageInQueue();
-    }
-
-    private void writeNextMessageInQueue() {
-        if (mPacketQueue.isEmpty()) {
-            logd(TAG, "No more packets to send.");
-            return;
-        }
-        boolean isLockAcquired = mIsSendingInProgress.compareAndSet(false, true);
-        if (!isLockAcquired) {
-            logd(TAG, "Unable to send packet at this time.");
-            return;
-        }
-
-        Packet packet = mPacketQueue.remove();
-        logd(TAG, "Writing packet " + packet.getPacketNumber() + " of "
-                + packet.getTotalPackets() + " for " + packet.getMessageId() + ".");
-        send(packet.toByteArray());
-    }
-
-    /** Process incoming data from stream. */
-    protected final void onDataReceived(byte[] data) {
-        if (!hasVersionBeenExchanged()) {
-            processVersionExchange(data);
-            return;
-        }
-
-        Packet packet;
-        try {
-            packet = Packet.parseFrom(data);
-        } catch (InvalidProtocolBufferException e) {
-            loge(TAG, "Can not parse packet from client.", e);
-            notifyMessageReceivedErrorListener(e);
-            return;
-        }
-        processPacket(packet);
-    }
-
-    protected final void processPacket(@NonNull Packet packet) {
-        int messageId = packet.getMessageId();
-        int packetNumber = packet.getPacketNumber();
-        int expectedPacket = mPendingPacketNumber.getOrDefault(messageId, 1);
-        if (packetNumber == expectedPacket - 1) {
-            logw(TAG, "Received duplicate packet " + packet.getPacketNumber() + " for message "
-                    + messageId + ". Ignoring.");
-            return;
-        }
-        if (packetNumber != expectedPacket) {
-            loge(TAG, "Received unexpected packet " + packetNumber + " for message "
-                    + messageId + ".");
-            notifyMessageReceivedErrorListener(
-                    new IllegalStateException("Packet received out of order."));
-            return;
-        }
-        mPendingPacketNumber.put(messageId, packetNumber + 1);
-
-        ByteArrayOutputStream currentPayloadStream =
-                mPendingData.getOrDefault(messageId, new ByteArrayOutputStream());
-        mPendingData.putIfAbsent(messageId, currentPayloadStream);
-
-        byte[] payload = packet.getPayload().toByteArray();
-        try {
-            currentPayloadStream.write(payload);
-        } catch (IOException e) {
-            loge(TAG, "Error writing packet to stream.", e);
-            notifyMessageReceivedErrorListener(e);
-            return;
-        }
-        logd(TAG, "Parsed packet " + packet.getPacketNumber() + " of "
-                + packet.getTotalPackets() + " for message " + messageId + ". Writing "
-                + payload.length + ".");
-
-        if (packet.getPacketNumber() != packet.getTotalPackets()) {
-            return;
-        }
-
-        byte[] messageBytes = currentPayloadStream.toByteArray();
-        mPendingData.remove(messageId);
-
-        logd(TAG, "Received complete device message " + messageId + " of " + messageBytes.length
-                + " bytes.");
-        Message message;
-        try {
-            message = Message.parseFrom(messageBytes);
-        } catch (InvalidProtocolBufferException e) {
-            loge(TAG, "Cannot parse device message from client.", e);
-            notifyMessageReceivedErrorListener(e);
-            return;
-        }
-
-        DeviceMessage deviceMessage = new DeviceMessage(
-                ByteUtils.bytesToUUID(message.getRecipient().toByteArray()),
-                message.getIsPayloadEncrypted(), message.getPayload().toByteArray());
-        notifyMessageReceivedListener(deviceMessage, message.getOperation());
-    }
-
-    /** The maximum amount of bytes that can be written in a single packet. */
-    public final void setMaxWriteSize(@IntRange(from = 1) int maxWriteSize) {
-        if (maxWriteSize <= 0) {
-            return;
-        }
-        mMaxWriteSize = maxWriteSize;
-    }
-
-    private boolean hasVersionBeenExchanged() {
-        return mIsVersionExchanged.get();
-    }
-
-    /** Indicate current send operation has completed. */
-    @VisibleForTesting
-    public final void sendCompleted() {
-        mIsSendingInProgress.set(false);
-        writeNextMessageInQueue();
-    }
-
-    private void processVersionExchange(@NonNull byte[] value) {
-        VersionExchange versionExchange;
-        try {
-            versionExchange = VersionExchange.parseFrom(value);
-        } catch (InvalidProtocolBufferException e) {
-            loge(TAG, "Could not parse version exchange message", e);
-            notifyMessageReceivedErrorListener(e);
-
-            return;
-        }
-        int minMessagingVersion = versionExchange.getMinSupportedMessagingVersion();
-        int maxMessagingVersion = versionExchange.getMaxSupportedMessagingVersion();
-        int minSecurityVersion = versionExchange.getMinSupportedSecurityVersion();
-        int maxSecurityVersion = versionExchange.getMaxSupportedSecurityVersion();
-        if (minMessagingVersion > MESSAGING_VERSION || maxMessagingVersion < MESSAGING_VERSION
-                || minSecurityVersion > SECURITY_VERSION || maxSecurityVersion < SECURITY_VERSION) {
-            loge(TAG, "Unsupported message version for min " + minMessagingVersion + " and max "
-                    + maxMessagingVersion + " or security version for " + minSecurityVersion
-                    + " and max " + maxSecurityVersion + ".");
-            notifyMessageReceivedErrorListener(new IllegalStateException("Unsupported version."));
-            return;
-        }
-
-        VersionExchange headunitVersion = VersionExchange.newBuilder()
-                .setMinSupportedMessagingVersion(MESSAGING_VERSION)
-                .setMaxSupportedMessagingVersion(MESSAGING_VERSION)
-                .setMinSupportedSecurityVersion(SECURITY_VERSION)
-                .setMaxSupportedSecurityVersion(SECURITY_VERSION)
-                .build();
-
-        send(headunitVersion.toByteArray());
-        mIsVersionExchanged.set(true);
-        logd(TAG, "Sent supported version to the phone.");
-    }
-
-
-    /** A generator of unique IDs for messages. */
-    private static class MessageIdGenerator {
-        private final AtomicInteger mMessageId = new AtomicInteger(0);
-
-        int next() {
-            int current = mMessageId.getAndIncrement();
-            mMessageId.compareAndSet(Integer.MAX_VALUE, 0);
-            return current;
-        }
-    }
-
-    /**
-     * Listener to be invoked when a complete message is received from the client.
-     */
-    public interface MessageReceivedListener {
-
-        /**
-         * Called when a complete message is received from the client.
-         *
-         * @param deviceMessage The message received from the client.
-         * @param operationType The {@link OperationType} of the received message.
-         */
-        void onMessageReceived(@NonNull DeviceMessage deviceMessage,
-                OperationType operationType);
-    }
-
-    /**
-     * Listener to be invoked when there was an error during receiving message from the client.
-     */
-    public interface MessageReceivedErrorListener {
-        /**
-         * Called when there was an error during receiving message from the client.
-         *
-         * @param exception The error.
-         */
-        void onMessageReceivedError(@NonNull Exception exception);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannel.java
deleted file mode 100644
index c6b0a7d..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannel.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static android.car.encryptionrunner.HandshakeMessage.HandshakeState;
-
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.HandshakeMessage;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-
-/**
- * A secure channel established with the association flow with an out-of-band verification.
- */
-public class OobAssociationSecureChannel extends AssociationSecureChannel {
-
-    private static final String TAG = "OobAssociationSecureChannel";
-
-    private final OobConnectionManager mOobConnectionManager;
-
-    private byte[] mOobCode;
-
-    public OobAssociationSecureChannel(
-            DeviceMessageStream stream,
-            ConnectedDeviceStorage storage,
-            OobConnectionManager oobConnectionManager) {
-        this(stream, storage, oobConnectionManager, EncryptionRunnerFactory.newRunner(
-                EncryptionRunnerFactory.EncryptionRunnerType.OOB_UKEY2));
-    }
-
-    OobAssociationSecureChannel(
-            DeviceMessageStream stream,
-            ConnectedDeviceStorage storage,
-            OobConnectionManager oobConnectionManager,
-            EncryptionRunner encryptionRunner) {
-        super(stream, storage, encryptionRunner);
-        mOobConnectionManager = oobConnectionManager;
-    }
-
-    @Override
-    void processHandshake(@NonNull byte[] message) throws HandshakeException {
-        switch (getState()) {
-            case HandshakeState.IN_PROGRESS:
-                processHandshakeInProgress(message);
-                break;
-            case HandshakeState.OOB_VERIFICATION_NEEDED:
-                processHandshakeOobVerificationNeeded(message);
-                break;
-            default:
-                super.processHandshake(message);
-        }
-    }
-
-    private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
-        HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
-        setState(handshakeMessage.getHandshakeState());
-        int state = getState();
-        if (state != HandshakeState.OOB_VERIFICATION_NEEDED) {
-            loge(TAG, "processHandshakeInProgress: Encountered unexpected handshake state: "
-                    + state + ".");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-            return;
-        }
-
-        mOobCode = handshakeMessage.getOobVerificationCode();
-        if (mOobCode == null) {
-            loge(TAG, "Unable to get out of band verification code.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
-            return;
-        }
-
-        byte[] encryptedCode;
-        try {
-            encryptedCode = mOobConnectionManager.encryptVerificationCode(mOobCode);
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException
-                | IllegalBlockSizeException | BadPaddingException e) {
-            loge(TAG, "Encryption failed for verification code exchange.", e);
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
-            return;
-        }
-
-        sendHandshakeMessage(encryptedCode, /* isEncrypted= */ false);
-    }
-
-    private void processHandshakeOobVerificationNeeded(@NonNull byte[] message) {
-        byte[] decryptedCode;
-        try {
-            decryptedCode = mOobConnectionManager.decryptVerificationCode(message);
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException
-                | IllegalBlockSizeException | BadPaddingException e) {
-            loge(TAG, "Decryption failed for verification code exchange", e);
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_HANDSHAKE);
-            return;
-        }
-
-        if (!Arrays.equals(mOobCode, decryptedCode)) {
-            loge(TAG, "Exchanged verification codes do not match. Aborting secure channel.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
-            return;
-        }
-
-        notifyOutOfBandAccepted();
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactory.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactory.java
deleted file mode 100644
index 5036a18..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactory.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.StreamProtos.PacketProto.Packet;
-import com.android.car.protobuf.ByteString;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Factory for creating {@link Packet} protos.
- */
-class PacketFactory {
-    private static final String TAG = "PacketFactory";
-
-    /**
-     * The size in bytes of a {@code fixed32} field in the proto.
-     */
-    private static final int FIXED_32_SIZE = 4;
-
-    /**
-     * The bytes needed to encode the field number in the proto.
-     *
-     * <p>Since the {@link Packet} only has 4 fields, it will only take 1 additional byte to
-     * encode.
-     */
-    private static final int FIELD_NUMBER_ENCODING_SIZE = 1;
-
-    /**
-     * The size in bytes of field {@code packet_number}. The proto field is a {@code fixed32}.
-     */
-    private static final int PACKET_NUMBER_ENCODING_SIZE =
-            FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE;
-
-    /**
-     * Split given data if necessary to fit within the given {@code maxSize}.
-     *
-     * @param payload The payload to potentially split across multiple {@link Packet}s.
-     * @param messageId The unique id for identifying message.
-     * @param maxSize The maximum size of each chunk.
-     * @return A list of {@link Packet}s.
-     * @throws PacketFactoryException if an error occurred during the splitting of data.
-     */
-    static List<Packet> makePackets(byte[] payload, int messageId, int maxSize)
-            throws PacketFactoryException {
-        List<Packet> blePackets = new ArrayList<>();
-        int payloadSize = payload.length;
-        int totalPackets = getTotalPacketNumber(messageId, payloadSize, maxSize);
-        int maxPayloadSize = maxSize
-                - getPacketHeaderSize(totalPackets, messageId, Math.min(payloadSize, maxSize));
-
-        int start = 0;
-        int end = Math.min(payloadSize, maxPayloadSize);
-        for (int packetNum = 1; packetNum <= totalPackets; packetNum++) {
-            blePackets.add(Packet.newBuilder()
-                    .setPacketNumber(packetNum)
-                    .setTotalPackets(totalPackets)
-                    .setMessageId(messageId)
-                    .setPayload(ByteString.copyFrom(Arrays.copyOfRange(payload, start, end)))
-                    .build());
-            start = end;
-            end = Math.min(start + maxPayloadSize, payloadSize);
-        }
-        return blePackets;
-    }
-
-    /**
-     * Compute the header size for the {@link Packet} proto in bytes. This method assumes that
-     * the proto contains a payload.
-     */
-    @VisibleForTesting
-    static int getPacketHeaderSize(int totalPackets, int messageId, int payloadSize) {
-        return FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE
-                + getEncodedSize(totalPackets) + FIELD_NUMBER_ENCODING_SIZE
-                + getEncodedSize(messageId) + FIELD_NUMBER_ENCODING_SIZE
-                + getEncodedSize(payloadSize) + FIELD_NUMBER_ENCODING_SIZE;
-    }
-
-    /**
-     * Compute the total packets required to encode a payload of the given size.
-     */
-    @VisibleForTesting
-    static int getTotalPacketNumber(int messageId, int payloadSize, int maxSize)
-            throws PacketFactoryException {
-        int headerSizeWithoutTotalPackets = FIXED_32_SIZE + FIELD_NUMBER_ENCODING_SIZE
-                + getEncodedSize(messageId) + FIELD_NUMBER_ENCODING_SIZE
-                + getEncodedSize(Math.min(payloadSize, maxSize)) + FIELD_NUMBER_ENCODING_SIZE;
-
-        for (int value = 1; value <= PACKET_NUMBER_ENCODING_SIZE; value++) {
-            int packetHeaderSize = headerSizeWithoutTotalPackets + value
-                    + FIELD_NUMBER_ENCODING_SIZE;
-            int maxPayloadSize = maxSize - packetHeaderSize;
-            if (maxPayloadSize < 0) {
-                throw new PacketFactoryException("Packet header size too large.");
-            }
-            int totalPackets = (int) Math.ceil(payloadSize / (double) maxPayloadSize);
-            if (getEncodedSize(totalPackets) == value) {
-                return totalPackets;
-            }
-        }
-
-        loge(TAG, "Cannot get valid total packet number for message: messageId: "
-                + messageId + ", payloadSize: " + payloadSize + ", maxSize: " + maxSize);
-        throw new PacketFactoryException("No valid total packet number.");
-    }
-
-    /**
-     * This method implements Protocol Buffers encoding algorithm.
-     *
-     * <p>Computes the number of bytes that would be needed to store a 32-bit variant.
-     *
-     * @param value the data that need to be encoded
-     * @return the size of the encoded data
-     * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding#varints">
-     *     Protocol Buffers Encoding</a>
-     */
-    private static int getEncodedSize(int value) {
-        if (value < 0) {
-            return 10;
-        }
-        if ((value & (~0 << 7)) == 0) {
-            return 1;
-        }
-        if ((value & (~0 << 14)) == 0) {
-            return 2;
-        }
-        if ((value & (~0 << 21)) == 0) {
-            return 3;
-        }
-        if ((value & (~0 << 28)) == 0) {
-            return 4;
-        }
-        return 5;
-    }
-
-    private PacketFactory() {}
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactoryException.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactoryException.java
deleted file mode 100644
index 41c11fc..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/PacketFactoryException.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.connecteddevice.connection;
-
-/**
- * Exception for signaling {@link PacketFactory} errors.
- */
-class PacketFactoryException extends Exception {
-    PacketFactoryException(String message) {
-        super(message);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ReconnectSecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ReconnectSecureChannel.java
deleted file mode 100644
index 71c879a..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ReconnectSecureChannel.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.HandshakeMessage;
-import android.car.encryptionrunner.HandshakeMessage.HandshakeState;
-import android.car.encryptionrunner.Key;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A secure channel established with the reconnection flow.
- */
-public class ReconnectSecureChannel extends SecureChannel {
-
-    private static final String TAG = "ReconnectSecureChannel";
-
-    private final ConnectedDeviceStorage mStorage;
-
-    private final String mDeviceId;
-
-    private final byte[] mExpectedChallengeResponse;
-
-    @HandshakeState
-    private int mState = HandshakeState.UNKNOWN;
-
-    private AtomicBoolean mHasVerifiedDevice = new AtomicBoolean(false);
-
-    /**
-     * Create a new secure reconnection channel.
-     *
-     * @param stream The {@link DeviceMessageStream} for communication with the device.
-     * @param storage {@link ConnectedDeviceStorage} for secure storage.
-     * @param deviceId Id of the device being reconnected.
-     * @param expectedChallengeResponse Expected response to challenge issued in reconnect. Should
-     *                                  pass {@code null} when device verification is not needed
-     *                                  during the reconnection process.
-     */
-    public ReconnectSecureChannel(@NonNull DeviceMessageStream stream,
-            @NonNull ConnectedDeviceStorage storage, @NonNull String deviceId,
-            @Nullable byte[] expectedChallengeResponse) {
-        super(stream, newReconnectRunner());
-        mStorage = storage;
-        mDeviceId = deviceId;
-        if (expectedChallengeResponse == null) {
-            // Skip the device verification step for spp reconnection
-            mHasVerifiedDevice.set(true);
-        }
-        mExpectedChallengeResponse = expectedChallengeResponse;
-    }
-
-    private static EncryptionRunner newReconnectRunner() {
-        EncryptionRunner encryptionRunner = EncryptionRunnerFactory.newRunner();
-        encryptionRunner.setIsReconnect(true);
-        return encryptionRunner;
-    }
-
-    @Override
-    void processHandshake(byte[] message) throws HandshakeException {
-        switch (mState) {
-            case HandshakeState.UNKNOWN:
-                if (!mHasVerifiedDevice.get()) {
-                    processHandshakeDeviceVerification(message);
-                } else {
-                    processHandshakeInitialization(message);
-                }
-                break;
-            case HandshakeState.IN_PROGRESS:
-                processHandshakeInProgress(message);
-                break;
-            case HandshakeState.RESUMING_SESSION:
-                processHandshakeResumingSession(message);
-                break;
-            default:
-                loge(TAG, "Encountered unexpected handshake state: " + mState + ".");
-                notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-        }
-    }
-
-    private void processHandshakeDeviceVerification(byte[] message) {
-        byte[] challengeResponse = Arrays.copyOf(message,
-                mExpectedChallengeResponse.length);
-        byte[] deviceChallenge = Arrays.copyOfRange(message,
-                mExpectedChallengeResponse.length, message.length);
-        if (!Arrays.equals(mExpectedChallengeResponse, challengeResponse)) {
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
-            return;
-        }
-        logd(TAG, "Responding to challenge " + ByteUtils.byteArrayToHexString(deviceChallenge)
-                + ".");
-        byte[] deviceChallengeResponse = mStorage.hashWithChallengeSecret(mDeviceId,
-                deviceChallenge);
-        if (deviceChallengeResponse == null) {
-            notifySecureChannelFailure(CHANNEL_ERROR_STORAGE_ERROR);
-        }
-        sendHandshakeMessage(deviceChallengeResponse, /* isEncrypted= */ false);
-        mHasVerifiedDevice.set(true);
-    }
-
-    private void processHandshakeInitialization(byte[] message) throws HandshakeException {
-        logd(TAG, "Responding to handshake init request.");
-        HandshakeMessage handshakeMessage = getEncryptionRunner().respondToInitRequest(message);
-        mState = handshakeMessage.getHandshakeState();
-        sendHandshakeMessage(handshakeMessage.getNextMessage(), /* isEncrypted= */ false);
-    }
-
-    private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
-        logd(TAG, "Continuing handshake.");
-        HandshakeMessage handshakeMessage = getEncryptionRunner().continueHandshake(message);
-        mState = handshakeMessage.getHandshakeState();
-    }
-
-    private void processHandshakeResumingSession(@NonNull byte[] message)
-            throws HandshakeException {
-        logd(TAG, "Start reconnection authentication.");
-
-        byte[] previousKey = mStorage.getEncryptionKey(mDeviceId);
-        if (previousKey == null) {
-            loge(TAG, "Unable to resume session, previous key is null.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
-            return;
-        }
-
-        HandshakeMessage handshakeMessage = getEncryptionRunner().authenticateReconnection(message,
-                previousKey);
-        mState = handshakeMessage.getHandshakeState();
-        if (mState != HandshakeState.FINISHED) {
-            loge(TAG, "Unable to resume session, unexpected next handshake state: " + mState + ".");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
-            return;
-        }
-
-        Key newKey = handshakeMessage.getKey();
-        if (newKey == null) {
-            loge(TAG, "Unable to resume session, new key is null.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_ENCRYPTION_KEY);
-            return;
-        }
-
-        logd(TAG, "Saved new key for reconnection.");
-        mStorage.saveEncryptionKey(mDeviceId, newKey.asBytes());
-        setEncryptionKey(newKey);
-        sendServerAuthToClient(handshakeMessage.getNextMessage());
-        notifyCallback(Callback::onSecureChannelEstablished);
-    }
-
-    private void sendServerAuthToClient(@Nullable byte[] message) {
-        if (message == null) {
-            loge(TAG, "Unable to send server authentication message to client, message is null.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
-            return;
-        }
-
-        sendHandshakeMessage(message, /* isEncrypted= */ false);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/SecureChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/SecureChannel.java
deleted file mode 100644
index e6ad290..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/SecureChannel.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.Key;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.security.SignatureException;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-
-/**
- * Establishes a secure channel with {@link EncryptionRunner} over {@link DeviceMessageStream} as
- * server side, sends and receives messages securely after the secure channel has been established.
- */
-public abstract class SecureChannel {
-
-    private static final String TAG = "SecureChannel";
-
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({
-            CHANNEL_ERROR_INVALID_HANDSHAKE,
-            CHANNEL_ERROR_INVALID_MSG,
-            CHANNEL_ERROR_INVALID_DEVICE_ID,
-            CHANNEL_ERROR_INVALID_VERIFICATION,
-            CHANNEL_ERROR_INVALID_STATE,
-            CHANNEL_ERROR_INVALID_ENCRYPTION_KEY,
-            CHANNEL_ERROR_STORAGE_ERROR
-    })
-    @interface ChannelError { }
-
-    /** Indicates an error during a Handshake of EncryptionRunner. */
-    static final int CHANNEL_ERROR_INVALID_HANDSHAKE = 0;
-    /** Received an invalid handshake message or has an invalid handshake message to send. */
-    static final int CHANNEL_ERROR_INVALID_MSG = 1;
-    /** Unable to retrieve a valid id. */
-    static final int CHANNEL_ERROR_INVALID_DEVICE_ID = 2;
-    /** Unable to get verification code or there's a error during pin verification. */
-    static final int CHANNEL_ERROR_INVALID_VERIFICATION = 3;
-    /** Encountered an unexpected handshake state. */
-    static final int CHANNEL_ERROR_INVALID_STATE = 4;
-    /** Failed to get a valid previous/new encryption key. */
-    static final int CHANNEL_ERROR_INVALID_ENCRYPTION_KEY = 5;
-    /** Failed to save or retrieve security keys. */
-    static final int CHANNEL_ERROR_STORAGE_ERROR = 6;
-
-
-    private final DeviceMessageStream mStream;
-
-    private final EncryptionRunner mEncryptionRunner;
-
-    private final AtomicReference<Key> mEncryptionKey = new AtomicReference<>();
-
-    private Callback mCallback;
-
-    SecureChannel(@NonNull DeviceMessageStream stream,
-            @NonNull EncryptionRunner encryptionRunner) {
-        mStream = stream;
-        mEncryptionRunner = encryptionRunner;
-        mStream.setMessageReceivedListener(this::onMessageReceived);
-    }
-
-    /** Logic for processing a handshake message from device. */
-    abstract void processHandshake(byte[] message) throws HandshakeException;
-
-    void sendHandshakeMessage(@Nullable byte[] message, boolean isEncrypted) {
-        if (message == null) {
-            loge(TAG, "Unable to send next handshake message, message is null.");
-            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
-            return;
-        }
-
-        logd(TAG, "Sending handshake message.");
-        DeviceMessage deviceMessage = new DeviceMessage(/* recipient= */ null,
-                isEncrypted, message);
-        if (deviceMessage.isMessageEncrypted()) {
-            encryptMessage(deviceMessage);
-        }
-        mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
-    }
-
-    /** Set the encryption key that secures this channel. */
-    void setEncryptionKey(@Nullable Key encryptionKey) {
-        mEncryptionKey.set(encryptionKey);
-    }
-
-    /**
-     * Send a client message.
-     * <p>Note: This should be called with an encrypted message only after the secure channel has
-     * been established.</p>
-     *
-     * @param deviceMessage The {@link DeviceMessage} to send.
-     */
-    public void sendClientMessage(@NonNull DeviceMessage deviceMessage)
-            throws IllegalStateException {
-        if (deviceMessage.isMessageEncrypted()) {
-            encryptMessage(deviceMessage);
-        }
-        mStream.writeMessage(deviceMessage, OperationType.CLIENT_MESSAGE);
-    }
-
-    private void encryptMessage(@NonNull DeviceMessage deviceMessage) {
-        Key key = mEncryptionKey.get();
-        if (key == null) {
-            throw new IllegalStateException("Secure channel has not been established.");
-        }
-
-        byte[] encryptedMessage = key.encryptData(deviceMessage.getMessage());
-        deviceMessage.setMessage(encryptedMessage);
-    }
-
-    /** Get the BLE stream backing this channel. */
-    @NonNull
-    public DeviceMessageStream getStream() {
-        return mStream;
-    }
-
-    /** Register a callback that notifies secure channel events. */
-    public void registerCallback(Callback callback) {
-        mCallback = callback;
-    }
-
-    /** Unregister a callback. */
-    void unregisterCallback(Callback callback) {
-        if (callback == mCallback) {
-            mCallback = null;
-        }
-    }
-
-    @VisibleForTesting
-    @Nullable
-    public Callback getCallback() {
-        return mCallback;
-    }
-
-    void notifyCallback(@NonNull Consumer<Callback> notification) {
-        if (mCallback != null) {
-            notification.accept(mCallback);
-        }
-    }
-
-    /** Notify callbacks that an error has occurred. */
-    void notifySecureChannelFailure(@ChannelError int error) {
-        loge(TAG, "Secure channel error: " + error);
-        notifyCallback(callback -> callback.onEstablishSecureChannelFailure(error));
-    }
-
-    /** Return the {@link EncryptionRunner} for this channel. */
-    @NonNull
-    EncryptionRunner getEncryptionRunner() {
-        return mEncryptionRunner;
-    }
-
-    /**
-     * Process the inner message and replace with decrypted value if necessary. If an error occurs
-     * the inner message will be replaced with {@code null} and call
-     * {@link Callback#onMessageReceivedError(Exception)} on the registered callback.
-     *
-     * @param deviceMessage The message to process.
-     * @return {@code true} if message was successfully processed. {@code false} if an error
-     * occurred.
-     */
-    @VisibleForTesting
-    boolean processMessage(@NonNull DeviceMessage deviceMessage) {
-        if (!deviceMessage.isMessageEncrypted()) {
-            logd(TAG, "Message was not decrypted. No further action necessary.");
-            return true;
-        }
-        Key key = mEncryptionKey.get();
-        if (key == null) {
-            loge(TAG, "Received encrypted message before secure channel has "
-                    + "been established.");
-            notifyCallback(callback -> callback.onMessageReceivedError(null));
-            deviceMessage.setMessage(null);
-            return false;
-        }
-        try {
-            byte[] decryptedMessage = key.decryptData(deviceMessage.getMessage());
-            deviceMessage.setMessage(decryptedMessage);
-            logd(TAG, "Decrypted secure message.");
-            return true;
-        } catch (SignatureException e) {
-            loge(TAG, "Could not decrypt client credentials.", e);
-            notifyCallback(callback -> callback.onMessageReceivedError(e));
-            deviceMessage.setMessage(null);
-
-            return false;
-        }
-    }
-
-    @VisibleForTesting
-    void onMessageReceived(@NonNull DeviceMessage deviceMessage, OperationType operationType) {
-        boolean success = processMessage(deviceMessage);
-        switch(operationType) {
-            case ENCRYPTION_HANDSHAKE:
-                if (!success) {
-                    notifyCallback(callback -> callback.onEstablishSecureChannelFailure(
-                            CHANNEL_ERROR_INVALID_HANDSHAKE));
-                    break;
-                }
-                logd(TAG, "Received handshake message.");
-                try {
-                    processHandshake(deviceMessage.getMessage());
-                } catch (HandshakeException e) {
-                    loge(TAG, "Handshake failed.", e);
-                    notifyCallback(callback -> callback.onEstablishSecureChannelFailure(
-                            CHANNEL_ERROR_INVALID_HANDSHAKE));
-                }
-                break;
-            case CLIENT_MESSAGE:
-                if (!success || deviceMessage.getMessage() == null) {
-                    break;
-                }
-                logd(TAG, "Received client message.");
-                notifyCallback(
-                        callback -> callback.onMessageReceived(deviceMessage));
-                break;
-            default:
-                loge(TAG, "Received unexpected operation type: " + operationType + ".");
-        }
-    }
-
-    /**
-     * Callbacks that will be invoked during establishing secure channel, sending and receiving
-     * messages securely.
-     */
-    public interface Callback {
-        /**
-         * Invoked when secure channel has been established successfully.
-         */
-        default void onSecureChannelEstablished() { }
-
-        /**
-         * Invoked when a {@link ChannelError} has been encountered in attempting to establish
-         * a secure channel.
-         *
-         * @param error The failure indication.
-         */
-        default void onEstablishSecureChannelFailure(@SecureChannel.ChannelError int error) { }
-
-        /**
-         * Invoked when a complete message is received securely from the client and decrypted.
-         *
-         * @param deviceMessage The {@link DeviceMessage} with decrypted message.
-         */
-        default void onMessageReceived(@NonNull DeviceMessage deviceMessage) { }
-
-        /**
-         * Invoked when there was an error during a processing or decrypting of a client message.
-         *
-         * @param exception The error.
-         */
-        default void onMessageReceivedError(@Nullable Exception exception) { }
-
-        /**
-         * Invoked when the device id was received from the client.
-         *
-         * @param deviceId The unique device id of client.
-         */
-        default void onDeviceIdReceived(@NonNull String deviceId) { }
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleCentralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleCentralManager.java
deleted file mode 100644
index 7a663f7..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleCentralManager.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Handler;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Class that manages BLE scanning operations.
- */
-public class BleCentralManager {
-
-    private static final String TAG = "BleCentralManager";
-
-    private static final int RETRY_LIMIT = 5;
-
-    private static final int RETRY_INTERVAL_MS = 1000;
-
-    private final Context mContext;
-
-    private final Handler mHandler;
-
-    private List<ScanFilter> mScanFilters;
-
-    private ScanSettings mScanSettings;
-
-    private ScanCallback mScanCallback;
-
-    private BluetoothLeScanner mScanner;
-
-    private int mScannerStartCount = 0;
-
-    private AtomicInteger mScannerState = new AtomicInteger(STOPPED);
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({
-            STOPPED,
-            STARTED,
-            SCANNING
-    })
-    private @interface ScannerState {}
-    private static final int STOPPED = 0;
-    private static final int STARTED = 1;
-    private static final int SCANNING = 2;
-
-    public BleCentralManager(@NonNull Context context) {
-        mContext = context;
-        mHandler = new Handler(context.getMainLooper());
-    }
-
-    /**
-     * Start the BLE scanning process.
-     *
-     * @param filters Optional list of {@link ScanFilter}s to apply to scan results.
-     * @param settings {@link ScanSettings} to apply to scanner.
-     * @param callback {@link ScanCallback} for scan events.
-     */
-    public void startScanning(@Nullable List<ScanFilter> filters, @NonNull ScanSettings settings,
-            @NonNull ScanCallback callback) {
-        if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
-            loge(TAG, "Attempted start scanning, but system does not support BLE. Ignoring");
-            return;
-        }
-        logd(TAG, "Request received to start scanning.");
-        mScannerStartCount = 0;
-        mScanFilters = filters;
-        mScanSettings = settings;
-        mScanCallback = callback;
-        updateScannerState(STARTED);
-        startScanningInternally();
-    }
-
-    /** Stop the scanner */
-    public void stopScanning() {
-        logd(TAG, "Attempting to stop scanning");
-        if (mScanner != null) {
-            mScanner.stopScan(mInternalScanCallback);
-        }
-        mScanCallback = null;
-        updateScannerState(STOPPED);
-    }
-
-    /** Returns {@code true} if currently scanning, {@code false} otherwise. */
-    public boolean isScanning() {
-        return mScannerState.get() == SCANNING;
-    }
-
-    /** Clean up the scanning process. */
-    public void cleanup() {
-        if (isScanning()) {
-            stopScanning();
-        }
-    }
-
-    private void startScanningInternally() {
-        logd(TAG, "Attempting to start scanning");
-        if (mScanner == null && BluetoothAdapter.getDefaultAdapter() != null) {
-            mScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
-        }
-        if (mScanner != null) {
-            mScanner.startScan(mScanFilters, mScanSettings, mInternalScanCallback);
-            updateScannerState(SCANNING);
-        } else {
-            mHandler.postDelayed(() -> {
-                // Keep trying
-                logd(TAG, "Scanner unavailable. Trying again.");
-                startScanningInternally();
-            }, RETRY_INTERVAL_MS);
-        }
-    }
-
-    private void updateScannerState(@ScannerState int newState) {
-        mScannerState.set(newState);
-    }
-
-    private final ScanCallback mInternalScanCallback = new ScanCallback() {
-        @Override
-        public void onScanResult(int callbackType, ScanResult result) {
-            if (mScanCallback != null) {
-                mScanCallback.onScanResult(callbackType, result);
-            }
-        }
-
-        @Override
-        public void onBatchScanResults(List<ScanResult> results) {
-            logd(TAG, "Batch scan found " + results.size() + " results.");
-            if (mScanCallback != null) {
-                mScanCallback.onBatchScanResults(results);
-            }
-        }
-
-        @Override
-        public void onScanFailed(int errorCode) {
-            if (mScannerStartCount >= RETRY_LIMIT) {
-                loge(TAG, "Cannot start BLE Scanner. Scanning Retry count: "
-                        + mScannerStartCount);
-                if (mScanCallback != null) {
-                    mScanCallback.onScanFailed(errorCode);
-                }
-                return;
-            }
-
-            mScannerStartCount++;
-            logw(TAG, "BLE Scanner failed to start. Error: "
-                    + errorCode
-                    + " Retry: "
-                    + mScannerStartCount);
-            switch(errorCode) {
-                case SCAN_FAILED_ALREADY_STARTED:
-                    // Scanner already started. Do nothing.
-                    break;
-                case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
-                case SCAN_FAILED_INTERNAL_ERROR:
-                    mHandler.postDelayed(BleCentralManager.this::startScanningInternally,
-                            RETRY_INTERVAL_MS);
-                    break;
-                default:
-                    // Ignore other codes.
-            }
-        }
-    };
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleDeviceMessageStream.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleDeviceMessageStream.java
deleted file mode 100644
index 864475f..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BleDeviceMessageStream.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.os.Handler;
-import android.os.Looper;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.connection.DeviceMessageStream;
-
-import java.util.concurrent.atomic.AtomicLong;
-
-/** BLE message stream to a device. */
-public class BleDeviceMessageStream extends DeviceMessageStream {
-
-    private static final String TAG = "BleDeviceMessageStream";
-
-    /*
-     * During bandwidth testing, it was discovered that allowing the stream to send as fast as it
-     * can blocked outgoing notifications from being received by the connected device. Adding a
-     * throttle to the outgoing messages alleviated this block and allowed both sides to
-     * send/receive in parallel successfully.
-     */
-    private static final long THROTTLE_DEFAULT_MS = 10L;
-    private static final long THROTTLE_WAIT_MS = 75L;
-
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
-
-    private final AtomicLong mThrottleDelay = new AtomicLong(THROTTLE_DEFAULT_MS);
-
-    private final BlePeripheralManager mBlePeripheralManager;
-
-    private final BluetoothDevice mDevice;
-
-    private final BluetoothGattCharacteristic mWriteCharacteristic;
-
-    private final BluetoothGattCharacteristic mReadCharacteristic;
-
-    BleDeviceMessageStream(@NonNull BlePeripheralManager blePeripheralManager,
-            @NonNull BluetoothDevice device,
-            @NonNull BluetoothGattCharacteristic writeCharacteristic,
-            @NonNull BluetoothGattCharacteristic readCharacteristic,
-            int defaultMaxWriteSize) {
-        super(defaultMaxWriteSize);
-        mBlePeripheralManager = blePeripheralManager;
-        mDevice = device;
-        mWriteCharacteristic = writeCharacteristic;
-        mReadCharacteristic = readCharacteristic;
-        mBlePeripheralManager.addOnCharacteristicWriteListener(this::onCharacteristicWrite);
-        mBlePeripheralManager.addOnCharacteristicReadListener(this::onCharacteristicRead);
-    }
-
-    @Override
-    protected void send(byte[] data) {
-        mWriteCharacteristic.setValue(data);
-        mBlePeripheralManager.notifyCharacteristicChanged(mDevice, mWriteCharacteristic,
-                /* confirm= */ false);
-    }
-
-    private void onCharacteristicRead(@NonNull BluetoothDevice device) {
-        if (!mDevice.equals(device)) {
-            logw(TAG, "Received a read notification from a device (" + device.getAddress()
-                    + ") that is not the expected device (" + mDevice.getAddress() + ") registered "
-                    + "to this stream. Ignoring.");
-            return;
-        }
-
-        logd(TAG, "Releasing lock on characteristic.");
-        sendCompleted();
-    }
-
-    private void onCharacteristicWrite(@NonNull BluetoothDevice device,
-            @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
-        logd(TAG, "Received a message from a device (" + device.getAddress() + ").");
-        if (!mDevice.equals(device)) {
-            logw(TAG, "Received a message from a device (" + device.getAddress() + ") that is not "
-                    + "the expected device (" + mDevice.getAddress() + ") registered to this "
-                    + "stream. Ignoring.");
-            return;
-        }
-
-        if (!characteristic.getUuid().equals(mReadCharacteristic.getUuid())) {
-            logw(TAG, "Received a write to a characteristic (" + characteristic.getUuid() + ") that"
-                    + " is not the expected UUID (" + mReadCharacteristic.getUuid() + "). "
-                    + "Ignoring.");
-            return;
-        }
-        onDataReceived(value);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BlePeripheralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BlePeripheralManager.java
deleted file mode 100644
index 645cecb..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/BlePeripheralManager.java
+++ /dev/null
@@ -1,534 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattServer;
-import android.bluetooth.BluetoothGattServerCallback;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.bluetooth.le.BluetoothLeAdvertiser;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Handler;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * A generic class that manages BLE peripheral operations like start/stop advertising, notifying
- * connects/disconnects and reading/writing values to GATT characteristics.
- */
-// TODO(b/123248433) This could move to a separate comms library.
-public class BlePeripheralManager {
-    private static final String TAG = "BlePeripheralManager";
-
-    private static final int BLE_RETRY_LIMIT = 5;
-    private static final int BLE_RETRY_INTERVAL_MS = 1000;
-
-    private static final int GATT_SERVER_RETRY_LIMIT = 20;
-    private static final int GATT_SERVER_RETRY_DELAY_MS = 200;
-
-    // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
-    // .service.generic_access.xml
-    private static final UUID GENERIC_ACCESS_PROFILE_UUID =
-            UUID.fromString("00001800-0000-1000-8000-00805f9b34fb");
-    // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth
-    // .characteristic.gap.device_name.xml
-    private static final UUID DEVICE_NAME_UUID =
-            UUID.fromString("00002a00-0000-1000-8000-00805f9b34fb");
-
-    private final Handler mHandler;
-
-    private final Context mContext;
-    private final Set<Callback> mCallbacks = new CopyOnWriteArraySet<>();
-    private final Set<OnCharacteristicWriteListener> mWriteListeners = new HashSet<>();
-    private final Set<OnCharacteristicReadListener> mReadListeners = new HashSet<>();
-    private final AtomicReference<BluetoothGattServer> mGattServer = new AtomicReference<>();
-    private final AtomicReference<BluetoothGatt> mBluetoothGatt = new AtomicReference<>();
-
-    private int mMtuSize = 20;
-
-    private BluetoothManager mBluetoothManager;
-    private AtomicReference<BluetoothLeAdvertiser> mAdvertiser = new AtomicReference<>();
-    private int mAdvertiserStartCount;
-    private int mGattServerRetryStartCount;
-    private BluetoothGattService mBluetoothGattService;
-    private AdvertiseCallback mAdvertiseCallback;
-    private AdvertiseData mAdvertiseData;
-    private AdvertiseData mScanResponse;
-
-    public BlePeripheralManager(Context context) {
-        mContext = context;
-        mHandler = new Handler(mContext.getMainLooper());
-    }
-
-    /**
-     * Registers the given callback to be notified of various events within the {@link
-     * BlePeripheralManager}.
-     *
-     * @param callback The callback to be notified.
-     */
-    void registerCallback(@NonNull Callback callback) {
-        mCallbacks.add(callback);
-    }
-
-    /**
-     * Unregisters a previously registered callback.
-     *
-     * @param callback The callback to unregister.
-     */
-    void unregisterCallback(@NonNull Callback callback) {
-        mCallbacks.remove(callback);
-    }
-
-    /**
-     * Adds a listener to be notified of a write to characteristics.
-     *
-     * @param listener The listener to invoke.
-     */
-    void addOnCharacteristicWriteListener(@NonNull OnCharacteristicWriteListener listener) {
-        mWriteListeners.add(listener);
-    }
-
-    /**
-     * Removes the given listener from being notified of characteristic writes.
-     *
-     * @param listener The listener to remove.
-     */
-    void removeOnCharacteristicWriteListener(@NonNull OnCharacteristicWriteListener listener) {
-        mWriteListeners.remove(listener);
-    }
-
-    /**
-     * Adds a listener to be notified of reads to characteristics.
-     *
-     * @param listener The listener to invoke.
-     */
-    void addOnCharacteristicReadListener(@NonNull OnCharacteristicReadListener listener) {
-        mReadListeners.add(listener);
-    }
-
-    /**
-     * Removes the given listener from being notified of characteristic reads.
-     *
-     * @param listener The listener to remove.
-     */
-    void removeOnCharacteristicReadistener(@NonNull OnCharacteristicReadListener listener) {
-        mReadListeners.remove(listener);
-    }
-
-    /**
-     * Returns the current MTU size.
-     *
-     * @return The size of the MTU in bytes.
-     */
-    int getMtuSize() {
-        return mMtuSize;
-    }
-
-    /**
-     * Starts the GATT server with the given {@link BluetoothGattService} and begins advertising.
-     *
-     * <p>It is possible that BLE service is still in TURNING_ON state when this method is invoked.
-     * Therefore, several retries will be made to ensure advertising is started.
-     *
-     * @param service           {@link BluetoothGattService} that will be discovered by clients
-     * @param data              {@link AdvertiseData} data to advertise
-     * @param scanResponse      {@link AdvertiseData} scan response
-     * @param advertiseCallback {@link AdvertiseCallback} callback for advertiser
-     */
-    void startAdvertising(
-            BluetoothGattService service, AdvertiseData data,
-            AdvertiseData scanResponse, AdvertiseCallback advertiseCallback) {
-        logd(TAG, "Request to start advertising with service " + service.getUuid() + ".");
-        if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
-            loge(TAG, "Attempted start advertising, but system does not support BLE. Ignoring.");
-            return;
-        }
-        // Clears previous session before starting advertising.
-        cleanup();
-        mBluetoothGattService = service;
-        mAdvertiseCallback = advertiseCallback;
-        mAdvertiseData = data;
-        mScanResponse = scanResponse;
-        mGattServerRetryStartCount = 0;
-        mBluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
-        mGattServer.set(mBluetoothManager.openGattServer(mContext, mGattServerCallback));
-        openGattServer();
-    }
-
-    /**
-     * Stops the GATT server from advertising.
-     *
-     * @param advertiseCallback The callback that is associated with the advertisement.
-     */
-    void stopAdvertising(AdvertiseCallback advertiseCallback) {
-        BluetoothLeAdvertiser advertiser = mAdvertiser.getAndSet(null);
-        if (advertiser != null) {
-            advertiser.stopAdvertising(advertiseCallback);
-            logd(TAG, "Advertising stopped.");
-        }
-    }
-
-    /**
-     * Notifies the characteristic change via {@link BluetoothGattServer}
-     */
-    void notifyCharacteristicChanged(
-            @NonNull BluetoothDevice device,
-            @NonNull BluetoothGattCharacteristic characteristic,
-            boolean confirm) {
-        BluetoothGattServer gattServer = mGattServer.get();
-        if (gattServer == null) {
-            return;
-        }
-
-        if (!gattServer.notifyCharacteristicChanged(device, characteristic, confirm)) {
-            loge(TAG, "notifyCharacteristicChanged failed");
-        }
-    }
-
-    /**
-     * Connect the Gatt server of the remote device to retrieve device name.
-     */
-    final void retrieveDeviceName(BluetoothDevice device) {
-        mBluetoothGatt.compareAndSet(null, device.connectGatt(mContext, false, mGattCallback));
-    }
-
-    /**
-     * Cleans up the BLE GATT server state.
-     */
-    void cleanup() {
-        logd(TAG, "Cleaning up manager.");
-        // Stops the advertiser, scanner and GATT server. This needs to be done to avoid leaks.
-        stopAdvertising(mAdvertiseCallback);
-        // Clears all registered listeners. IHU only supports single connection in peripheral role.
-        mReadListeners.clear();
-        mWriteListeners.clear();
-
-        BluetoothGattServer gattServer = mGattServer.getAndSet(null);
-        if (gattServer == null) {
-            return;
-        }
-
-        logd(TAG, "Stopping gatt server.");
-        BluetoothGatt bluetoothGatt = mBluetoothGatt.getAndSet(null);
-        if (bluetoothGatt != null) {
-            gattServer.cancelConnection(bluetoothGatt.getDevice());
-            logd(TAG, "Disconnecting gatt.");
-            bluetoothGatt.disconnect();
-            bluetoothGatt.close();
-        }
-        gattServer.clearServices();
-        gattServer.close();
-    }
-
-    private void openGattServer() {
-        // Only open one Gatt server.
-        BluetoothGattServer gattServer = mGattServer.get();
-        if (gattServer != null) {
-            logd(TAG, "Gatt Server created, retry count: " + mGattServerRetryStartCount);
-            gattServer.clearServices();
-            gattServer.addService(mBluetoothGattService);
-            AdvertiseSettings settings =
-                    new AdvertiseSettings.Builder()
-                            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
-                            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
-                            .setConnectable(true)
-                            .build();
-            mAdvertiserStartCount = 0;
-            startAdvertisingInternally(settings, mAdvertiseData, mScanResponse, mAdvertiseCallback);
-            mGattServerRetryStartCount = 0;
-        } else if (mGattServerRetryStartCount < GATT_SERVER_RETRY_LIMIT) {
-            mGattServer.set(mBluetoothManager.openGattServer(mContext, mGattServerCallback));
-            mGattServerRetryStartCount++;
-            mHandler.postDelayed(() -> openGattServer(), GATT_SERVER_RETRY_DELAY_MS);
-        } else {
-            loge(TAG, "Gatt server not created - exceeded retry limit.");
-        }
-    }
-
-    private void startAdvertisingInternally(
-            AdvertiseSettings settings, AdvertiseData advertisement,
-            AdvertiseData scanResponse, AdvertiseCallback advertiseCallback) {
-        if (BluetoothAdapter.getDefaultAdapter() != null) {
-            mAdvertiser.compareAndSet(null,
-                    BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser());
-        }
-        BluetoothLeAdvertiser advertiser = mAdvertiser.get();
-        if (advertiser != null) {
-            logd(TAG, "Advertiser created, retry count: " + mAdvertiserStartCount);
-            advertiser.startAdvertising(settings, advertisement, scanResponse, advertiseCallback);
-            mAdvertiserStartCount = 0;
-        } else if (mAdvertiserStartCount < BLE_RETRY_LIMIT) {
-            mHandler.postDelayed(
-                    () -> startAdvertisingInternally(settings, advertisement, scanResponse,
-                            advertiseCallback), BLE_RETRY_INTERVAL_MS);
-            mAdvertiserStartCount += 1;
-        } else {
-            loge(TAG, "Cannot start BLE Advertisement. Advertise Retry count: "
-                            + mAdvertiserStartCount);
-        }
-    }
-
-    private final BluetoothGattServerCallback mGattServerCallback =
-            new BluetoothGattServerCallback() {
-                @Override
-                public void onConnectionStateChange(BluetoothDevice device, int status,
-                                                    int newState) {
-                    switch (newState) {
-                        case BluetoothProfile.STATE_CONNECTED:
-                            logd(TAG, "BLE Connection State Change: CONNECTED");
-                            BluetoothGattServer gattServer = mGattServer.get();
-                            if (gattServer == null) {
-                                return;
-                            }
-                            gattServer.connect(device, /* autoConnect= */ false);
-                            for (Callback callback : mCallbacks) {
-                                callback.onRemoteDeviceConnected(device);
-                            }
-                            break;
-                        case BluetoothProfile.STATE_DISCONNECTED:
-                            logd(TAG, "BLE Connection State Change: DISCONNECTED");
-                            for (Callback callback : mCallbacks) {
-                                callback.onRemoteDeviceDisconnected(device);
-                            }
-                            break;
-                        default:
-                            logw(TAG, "Connection state not connecting or disconnecting; ignoring: "
-                                    + newState);
-                    }
-                }
-
-                @Override
-                public void onServiceAdded(int status, BluetoothGattService service) {
-                    logd(TAG, "Service added status: " + status + " uuid: " + service.getUuid());
-                }
-
-                @Override
-                public void onCharacteristicWriteRequest(
-                        BluetoothDevice device,
-                        int requestId,
-                        BluetoothGattCharacteristic characteristic,
-                        boolean preparedWrite,
-                        boolean responseNeeded,
-                        int offset,
-                        byte[] value) {
-                    BluetoothGattServer gattServer = mGattServer.get();
-                    if (gattServer == null) {
-                        return;
-                    }
-                    gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
-                            value);
-                    for (OnCharacteristicWriteListener listener : mWriteListeners) {
-                        listener.onCharacteristicWrite(device, characteristic, value);
-                    }
-                }
-
-                @Override
-                public void onDescriptorWriteRequest(
-                        BluetoothDevice device,
-                        int requestId,
-                        BluetoothGattDescriptor descriptor,
-                        boolean preparedWrite,
-                        boolean responseNeeded,
-                        int offset,
-                        byte[] value) {
-                    logd(TAG, "Write request for descriptor: "
-                            + descriptor.getUuid()
-                            + "; value: "
-                            + ByteUtils.byteArrayToHexString(value));
-                    BluetoothGattServer gattServer = mGattServer.get();
-                    if (gattServer == null) {
-                        return;
-                    }
-                    gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
-                            value);
-                }
-
-                @Override
-                public void onMtuChanged(BluetoothDevice device, int mtu) {
-                    logd(TAG, "onMtuChanged: " + mtu + " for device " + device.getAddress());
-
-                    mMtuSize = mtu;
-
-                    for (Callback callback : mCallbacks) {
-                        callback.onMtuSizeChanged(mtu);
-                    }
-                }
-
-                @Override
-                public void onNotificationSent(BluetoothDevice device, int status) {
-                    super.onNotificationSent(device, status);
-                    if (status == BluetoothGatt.GATT_SUCCESS) {
-                        logd(TAG, "Notification sent successfully. Device: " + device.getAddress()
-                                + ", Status: " + status + ". Notifying all listeners.");
-                        for (OnCharacteristicReadListener listener : mReadListeners) {
-                            listener.onCharacteristicRead(device);
-                        }
-                    } else {
-                        loge(TAG, "Notification failed. Device: " + device + ", Status: "
-                                + status);
-                    }
-                }
-            };
-
-    private final BluetoothGattCallback mGattCallback =
-            new BluetoothGattCallback() {
-                @Override
-                public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
-                    logd(TAG, "Gatt Connection State Change: " + newState);
-                    switch (newState) {
-                        case BluetoothProfile.STATE_CONNECTED:
-                            logd(TAG, "Gatt connected");
-                            BluetoothGatt bluetoothGatt = mBluetoothGatt.get();
-                            if (bluetoothGatt == null) {
-                                break;
-                            }
-                            bluetoothGatt.discoverServices();
-                            break;
-                        case BluetoothProfile.STATE_DISCONNECTED:
-                            logd(TAG, "Gatt Disconnected");
-                            break;
-                        default:
-                            logd(TAG, "Connection state not connecting or disconnecting; ignoring: "
-                                    + newState);
-                    }
-                }
-
-                @Override
-                public void onServicesDiscovered(BluetoothGatt gatt, int status) {
-                    logd(TAG, "Gatt Services Discovered");
-                    BluetoothGatt bluetoothGatt = mBluetoothGatt.get();
-                    if (bluetoothGatt == null) {
-                        return;
-                    }
-                    BluetoothGattService gapService = bluetoothGatt.getService(
-                            GENERIC_ACCESS_PROFILE_UUID);
-                    if (gapService == null) {
-                        loge(TAG, "Generic Access Service is null.");
-                        return;
-                    }
-                    BluetoothGattCharacteristic deviceNameCharacteristic =
-                            gapService.getCharacteristic(DEVICE_NAME_UUID);
-                    if (deviceNameCharacteristic == null) {
-                        loge(TAG, "Device Name Characteristic is null.");
-                        return;
-                    }
-                    bluetoothGatt.readCharacteristic(deviceNameCharacteristic);
-                }
-
-                @Override
-                public void onCharacteristicRead(
-                        BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
-                        int status) {
-                    if (status == BluetoothGatt.GATT_SUCCESS) {
-                        String deviceName = characteristic.getStringValue(0);
-                        logd(TAG, "BLE Device Name: " + deviceName);
-
-                        for (Callback callback : mCallbacks) {
-                            callback.onDeviceNameRetrieved(deviceName);
-                        }
-                    } else {
-                        loge(TAG, "Reading GAP Failed: " + status);
-                    }
-                }
-            };
-
-    /**
-     * Interface to be notified of various events within the {@link BlePeripheralManager}.
-     */
-    interface Callback {
-        /**
-         * Triggered when the name of the remote device is retrieved.
-         *
-         * @param deviceName Name of the remote device.
-         */
-        void onDeviceNameRetrieved(@Nullable String deviceName);
-
-        /**
-         * Triggered if a remote client has requested to change the MTU for a given connection.
-         *
-         * @param size The new MTU size.
-         */
-        void onMtuSizeChanged(int size);
-
-        /**
-         * Triggered when a device (GATT client) connected.
-         *
-         * @param device Remote device that connected on BLE.
-         */
-        void onRemoteDeviceConnected(@NonNull BluetoothDevice device);
-
-        /**
-         * Triggered when a device (GATT client) disconnected.
-         *
-         * @param device Remote device that disconnected on BLE.
-         */
-        void onRemoteDeviceDisconnected(@NonNull BluetoothDevice device);
-    }
-
-    /**
-     * An interface for classes that wish to be notified of writes to a characteristic.
-     */
-    interface OnCharacteristicWriteListener {
-        /**
-         * Triggered when this BlePeripheralManager receives a write request from a remote device.
-         *
-         * @param device         The bluetooth device that holds the characteristic.
-         * @param characteristic The characteristic that was written to.
-         * @param value          The value that was written.
-         */
-        void onCharacteristicWrite(
-                @NonNull BluetoothDevice device,
-                @NonNull BluetoothGattCharacteristic characteristic,
-                @NonNull byte[] value);
-    }
-
-    /**
-     * An interface for classes that wish to be notified of reads on a characteristic.
-     */
-    interface OnCharacteristicReadListener {
-        /**
-         * Triggered when this BlePeripheralManager receives a read request from a remote device.
-         *
-         * @param device The bluetooth device that holds the characteristic.
-         */
-        void onCharacteristicRead(@NonNull BluetoothDevice device);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBleCentralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBleCentralManager.java
deleted file mode 100644
index e8d17e5..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBleCentralManager.java
+++ /dev/null
@@ -1,382 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-import static com.android.car.connecteddevice.util.ScanDataAnalyzer.containsUuidsInOverflow;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanRecord;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.os.ParcelUuid;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-
-import java.math.BigInteger;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-/**
- * Communication manager for a car that maintains continuous connections with all devices in the car
- * for the duration of a drive.
- */
-public class CarBleCentralManager extends CarBluetoothManager {
-
-    private static final String TAG = "CarBleCentralManager";
-
-    // system/bt/internal_include/bt_target.h#GATT_MAX_PHY_CHANNEL
-    private static final int MAX_CONNECTIONS = 7;
-
-    private static final UUID CHARACTERISTIC_CONFIG =
-            UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
-
-    private static final int STATUS_FORCED_DISCONNECT = -1;
-
-    private final ScanSettings mScanSettings = new ScanSettings.Builder()
-            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
-            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
-            .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
-            .build();
-
-    private final CopyOnWriteArraySet<ConnectedRemoteDevice> mIgnoredDevices =
-            new CopyOnWriteArraySet<>();
-
-    private final Context mContext;
-
-    private final BleCentralManager mBleCentralManager;
-
-    private final UUID mServiceUuid;
-
-    private final UUID mWriteCharacteristicUuid;
-
-    private final UUID mReadCharacteristicUuid;
-
-    private final BigInteger mParsedBgServiceBitMask;
-
-    /**
-     * Create a new manager.
-     *
-     * @param context The caller's [Context].
-     * @param bleCentralManager [BleCentralManager] for establishing connections.
-     * @param connectedDeviceStorage Shared [ConnectedDeviceStorage] for companion features.
-     * @param serviceUuid [UUID] of peripheral's service.
-     * @param bgServiceMask iOS overflow bit mask for service UUID.
-     * @param writeCharacteristicUuid [UUID] of characteristic the car will write to.
-     * @param readCharacteristicUuid [UUID] of characteristic the device will write to.
-     */
-    public CarBleCentralManager(
-            @NonNull Context context,
-            @NonNull BleCentralManager bleCentralManager,
-            @NonNull ConnectedDeviceStorage connectedDeviceStorage,
-            @NonNull UUID serviceUuid,
-            @NonNull String bgServiceMask,
-            @NonNull UUID writeCharacteristicUuid,
-            @NonNull UUID readCharacteristicUuid) {
-        super(connectedDeviceStorage);
-        mContext = context;
-        mBleCentralManager = bleCentralManager;
-        mServiceUuid = serviceUuid;
-        mWriteCharacteristicUuid = writeCharacteristicUuid;
-        mReadCharacteristicUuid = readCharacteristicUuid;
-        mParsedBgServiceBitMask = new BigInteger(bgServiceMask, 16);
-    }
-
-    @Override
-    public void start() {
-        super.start();
-        mBleCentralManager.startScanning(/* filters= */ null, mScanSettings, mScanCallback);
-    }
-
-    @Override
-    public void stop() {
-        super.stop();
-        mBleCentralManager.stopScanning();
-    }
-
-    @Override
-    public void disconnectDevice(String deviceId) {
-        logd(TAG, "Request to disconnect from device " + deviceId + ".");
-        ConnectedRemoteDevice device = getConnectedDevice(deviceId);
-        if (device == null) {
-            return;
-        }
-
-        deviceDisconnected(device, STATUS_FORCED_DISCONNECT);
-    }
-
-    //TODO(b/141312136): Support car central role
-    @Override
-    public AssociationCallback getAssociationCallback() {
-        return null;
-    }
-
-    @Override
-    public void setAssociationCallback(AssociationCallback callback) {
-
-    }
-
-    @Override
-    public void connectToDevice(UUID deviceId) {
-
-    }
-
-    @Override
-    public void initiateConnectionToDevice(UUID deviceId) {
-
-    }
-
-    @Override
-    public void startAssociation(String nameForAssociation, AssociationCallback callback) {
-
-    }
-
-    @Override
-    public void startOutOfBandAssociation(String nameForAssociation, OobChannel oobChannel,
-            AssociationCallback callback) {
-
-    }
-
-    private void ignoreDevice(@NonNull ConnectedRemoteDevice device) {
-        mIgnoredDevices.add(device);
-    }
-
-    private boolean isDeviceIgnored(@NonNull BluetoothDevice device) {
-        for (ConnectedRemoteDevice connectedDevice : mIgnoredDevices) {
-            if (device.equals(connectedDevice.mDevice)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean shouldAttemptConnection(@NonNull ScanResult result) {
-        // Ignore any results that are not connectable.
-        if (!result.isConnectable()) {
-            return false;
-        }
-
-        // Do not attempt to connect if we have already hit our max. This should rarely happen
-        // and is protecting against a race condition of scanning stopped and new results coming in.
-        if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
-            return false;
-        }
-
-        BluetoothDevice device = result.getDevice();
-
-        // Do not connect if device has already been ignored.
-        if (isDeviceIgnored(device)) {
-            return false;
-        }
-
-        // Check if already attempting to connect to this device.
-        if (getConnectedDevice(device) != null) {
-            return false;
-        }
-
-
-        // Ignore any device without a scan record.
-        ScanRecord scanRecord = result.getScanRecord();
-        if (scanRecord == null) {
-            return false;
-        }
-
-        // Connect to any device that is advertising our service UUID.
-        List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
-        if (serviceUuids != null) {
-            for (ParcelUuid serviceUuid : serviceUuids) {
-                if (serviceUuid.getUuid().equals(mServiceUuid)) {
-                    return true;
-                }
-            }
-        }
-        if (containsUuidsInOverflow(scanRecord.getBytes(), mParsedBgServiceBitMask)) {
-            return true;
-        }
-
-        // Can safely ignore devices advertising unrecognized service uuids.
-        if (serviceUuids != null && !serviceUuids.isEmpty()) {
-            return false;
-        }
-
-        // TODO(b/139066293): Current implementation quickly exhausts connections resulting in
-        // greatly reduced performance for connecting to devices we know we want to connect to.
-        // Return true once fixed.
-        return false;
-    }
-
-    private void startDeviceConnection(@NonNull BluetoothDevice device) {
-        BluetoothGatt gatt = device.connectGatt(mContext, /* autoConnect= */ false,
-                mConnectionCallback, BluetoothDevice.TRANSPORT_LE);
-        if (gatt == null) {
-            return;
-        }
-
-        ConnectedRemoteDevice bleDevice = new ConnectedRemoteDevice(device, gatt);
-        bleDevice.mState = ConnectedDeviceState.CONNECTING;
-        addConnectedDevice(bleDevice);
-
-        // Stop scanning if we have reached the maximum number of connections.
-        if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
-            mBleCentralManager.stopScanning();
-        }
-    }
-
-    private void deviceConnected(@NonNull ConnectedRemoteDevice device) {
-        if (device.mGatt == null) {
-            loge(TAG, "Device connected with null gatt. Disconnecting.");
-            deviceDisconnected(device, BluetoothProfile.STATE_DISCONNECTED);
-            return;
-        }
-        device.mState = ConnectedDeviceState.PENDING_VERIFICATION;
-        device.mGatt.discoverServices();
-        logd(TAG, "New device connected: " + device.mGatt.getDevice().getAddress()
-                + ". Active connections: " + getConnectedDevicesCount() + ".");
-    }
-
-    private void deviceDisconnected(@NonNull ConnectedRemoteDevice device, int status) {
-        removeConnectedDevice(device);
-        if (device.mGatt != null) {
-            device.mGatt.close();
-        }
-        if (device.mDeviceId != null) {
-            mCallbacks.invoke(callback -> callback.onDeviceDisconnected(device.mDeviceId));
-        }
-        logd(TAG, "Device with id " + device.mDeviceId + " disconnected with state " + status
-                + ". Remaining active connections: " + getConnectedDevicesCount() + ".");
-    }
-
-    private final ScanCallback mScanCallback = new ScanCallback() {
-        @Override
-        public void onScanResult(int callbackType, ScanResult result) {
-            super.onScanResult(callbackType, result);
-            if (shouldAttemptConnection(result)) {
-                startDeviceConnection(result.getDevice());
-            }
-        }
-
-        @Override
-        public void onScanFailed(int errorCode) {
-            super.onScanFailed(errorCode);
-            loge(TAG, "BLE scanning failed with error code: " + errorCode);
-        }
-    };
-
-    private final BluetoothGattCallback mConnectionCallback = new BluetoothGattCallback() {
-        @Override
-        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
-            super.onConnectionStateChange(gatt, status, newState);
-            if (gatt == null) {
-                logw(TAG, "Null gatt passed to onConnectionStateChange. Ignoring.");
-                return;
-            }
-
-            ConnectedRemoteDevice connectedDevice = getConnectedDevice(gatt);
-            if (connectedDevice == null) {
-                return;
-            }
-
-            switch (newState) {
-                case BluetoothProfile.STATE_CONNECTED:
-                    deviceConnected(connectedDevice);
-                    break;
-                case BluetoothProfile.STATE_DISCONNECTED:
-                    deviceDisconnected(connectedDevice, status);
-                    break;
-                default:
-                    logd(TAG, "Connection state changed. New state: " + newState + " status: "
-                            + status);
-            }
-        }
-
-        @Override
-        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
-            super.onServicesDiscovered(gatt, status);
-            if (gatt == null) {
-                logw(TAG, "Null gatt passed to onServicesDiscovered. Ignoring.");
-                return;
-            }
-
-            ConnectedRemoteDevice connectedDevice = getConnectedDevice(gatt);
-            if (connectedDevice == null) {
-                return;
-            }
-            BluetoothGattService service = gatt.getService(mServiceUuid);
-            if (service == null) {
-                ignoreDevice(connectedDevice);
-                gatt.disconnect();
-                return;
-            }
-
-            connectedDevice.mState = ConnectedDeviceState.CONNECTED;
-            BluetoothGattCharacteristic writeCharacteristic =
-                    service.getCharacteristic(mWriteCharacteristicUuid);
-            BluetoothGattCharacteristic readCharacteristic =
-                    service.getCharacteristic(mReadCharacteristicUuid);
-            if (writeCharacteristic == null || readCharacteristic == null) {
-                logw(TAG, "Unable to find expected characteristics on peripheral.");
-                gatt.disconnect();
-                return;
-            }
-
-            // Turn on notifications for read characteristic.
-            BluetoothGattDescriptor descriptor =
-                    readCharacteristic.getDescriptor(CHARACTERISTIC_CONFIG);
-            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
-            if (!gatt.writeDescriptor(descriptor)) {
-                loge(TAG, "Write descriptor to read characteristic failed.");
-                gatt.disconnect();
-                return;
-            }
-
-            if (!gatt.setCharacteristicNotification(readCharacteristic, /* enable= */ true)) {
-                loge(TAG, "Set notifications to read characteristic failed.");
-                gatt.disconnect();
-                return;
-            }
-
-            logd(TAG, "Service and characteristics successfully discovered.");
-        }
-
-        @Override
-        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
-                int status) {
-            super.onDescriptorWrite(gatt, descriptor, status);
-            if (gatt == null) {
-                logw(TAG, "Null gatt passed to onDescriptorWrite. Ignoring.");
-                return;
-            }
-            // TODO(b/141312136): Create SecureBleChannel and assign to connectedDevice.
-        }
-    };
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManager.java
deleted file mode 100644
index a05b5f7..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManager.java
+++ /dev/null
@@ -1,510 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.ble;
-
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.ParcelUuid;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.AssociationSecureChannel;
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.connection.OobAssociationSecureChannel;
-import com.android.car.connecteddevice.connection.ReconnectSecureChannel;
-import com.android.car.connecteddevice.connection.SecureChannel;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-import com.android.car.connecteddevice.util.EventLog;
-
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.UUID;
-
-/**
- * Communication manager that allows for targeted connections to a specific device in the car.
- */
-public class CarBlePeripheralManager extends CarBluetoothManager {
-
-    private static final String TAG = "CarBlePeripheralManager";
-
-    // Attribute protocol bytes attached to message. Available write size is MTU size minus att
-    // bytes.
-    private static final int ATT_PROTOCOL_BYTES = 3;
-
-    private static final UUID CLIENT_CHARACTERISTIC_CONFIG =
-            UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
-
-    private static final int SALT_BYTES = 8;
-
-    private static final int TOTAL_AD_DATA_BYTES = 16;
-
-    private static final int TRUNCATED_BYTES = 3;
-
-    private static final String TIMEOUT_HANDLER_THREAD_NAME = "peripheralThread";
-
-    private final BluetoothGattDescriptor mDescriptor =
-            new BluetoothGattDescriptor(CLIENT_CHARACTERISTIC_CONFIG,
-                    BluetoothGattDescriptor.PERMISSION_READ
-                            | BluetoothGattDescriptor.PERMISSION_WRITE);
-
-    private final BlePeripheralManager mBlePeripheralManager;
-
-    private final UUID mAssociationServiceUuid;
-
-    private final UUID mReconnectServiceUuid;
-
-    private final UUID mReconnectDataUuid;
-
-    private final BluetoothGattCharacteristic mWriteCharacteristic;
-
-    private final BluetoothGattCharacteristic mReadCharacteristic;
-
-    private HandlerThread mTimeoutHandlerThread;
-
-    private Handler mTimeoutHandler;
-
-    private final Duration mMaxReconnectAdvertisementDuration;
-
-    private final int mDefaultMtuSize;
-
-    private String mReconnectDeviceId;
-
-    private byte[] mReconnectChallenge;
-
-    private AdvertiseCallback mAdvertiseCallback;
-
-    private OobConnectionManager mOobConnectionManager;
-
-    private AssociationCallback mAssociationCallback;
-
-    /**
-     * Initialize a new instance of manager.
-     *
-     * @param blePeripheralManager    {@link BlePeripheralManager} for establishing connection.
-     * @param connectedDeviceStorage  Shared {@link ConnectedDeviceStorage} for companion features.
-     * @param associationServiceUuid  {@link UUID} of association service.
-     * @param reconnectServiceUuid    {@link UUID} of reconnect service.
-     * @param reconnectDataUuid       {@link UUID} key of reconnect advertisement data.
-     * @param writeCharacteristicUuid {@link UUID} of characteristic the car will write to.
-     * @param readCharacteristicUuid  {@link UUID} of characteristic the device will write to.
-     * @param maxReconnectAdvertisementDuration Maximum duration to advertise for reconnect before
-     *                                          restarting.
-     * @param defaultMtuSize          Default MTU size for new channels.
-     */
-    public CarBlePeripheralManager(@NonNull BlePeripheralManager blePeripheralManager,
-            @NonNull ConnectedDeviceStorage connectedDeviceStorage,
-            @NonNull UUID associationServiceUuid,
-            @NonNull UUID reconnectServiceUuid,
-            @NonNull UUID reconnectDataUuid,
-            @NonNull UUID writeCharacteristicUuid,
-            @NonNull UUID readCharacteristicUuid,
-            @NonNull Duration maxReconnectAdvertisementDuration,
-            int defaultMtuSize) {
-        super(connectedDeviceStorage);
-        mBlePeripheralManager = blePeripheralManager;
-        mAssociationServiceUuid = associationServiceUuid;
-        mReconnectServiceUuid = reconnectServiceUuid;
-        mReconnectDataUuid = reconnectDataUuid;
-        mDescriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
-        mWriteCharacteristic = new BluetoothGattCharacteristic(writeCharacteristicUuid,
-                BluetoothGattCharacteristic.PROPERTY_NOTIFY,
-                BluetoothGattCharacteristic.PROPERTY_READ);
-        mReadCharacteristic = new BluetoothGattCharacteristic(readCharacteristicUuid,
-                BluetoothGattCharacteristic.PROPERTY_WRITE
-                        | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
-                BluetoothGattCharacteristic.PERMISSION_WRITE);
-        mReadCharacteristic.addDescriptor(mDescriptor);
-        mMaxReconnectAdvertisementDuration = maxReconnectAdvertisementDuration;
-        mDefaultMtuSize = defaultMtuSize;
-    }
-
-    @Override
-    public void start() {
-        super.start();
-        mTimeoutHandlerThread = new HandlerThread(TIMEOUT_HANDLER_THREAD_NAME);
-        mTimeoutHandlerThread.start();
-        mTimeoutHandler = new Handler(mTimeoutHandlerThread.getLooper());
-    }
-
-    @Override
-    public void stop() {
-        super.stop();
-        if (mTimeoutHandlerThread != null) {
-            mTimeoutHandlerThread.quit();
-        }
-        reset();
-    }
-
-    @Override
-    public void disconnectDevice(@NonNull String deviceId) {
-        if (deviceId.equals(mReconnectDeviceId)) {
-            logd(TAG, "Reconnection canceled for device " + deviceId + ".");
-            reset();
-            return;
-        }
-        ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-        if (connectedDevice == null || !deviceId.equals(connectedDevice.mDeviceId)) {
-            return;
-        }
-        reset();
-    }
-
-    @Override
-    public AssociationCallback getAssociationCallback() {
-        return mAssociationCallback;
-    }
-
-    @Override
-    public void setAssociationCallback(AssociationCallback callback) {
-        mAssociationCallback = callback;
-    }
-
-    @Override
-    public void reset() {
-        super.reset();
-        logd(TAG, "Resetting state.");
-        mBlePeripheralManager.cleanup();
-        mReconnectDeviceId = null;
-        mReconnectChallenge = null;
-        mOobConnectionManager = null;
-        mAssociationCallback = null;
-    }
-
-    @Override
-    public void initiateConnectionToDevice(@NonNull UUID deviceId) {
-        mReconnectDeviceId = deviceId.toString();
-        mAdvertiseCallback = new AdvertiseCallback() {
-            @Override
-            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
-                super.onStartSuccess(settingsInEffect);
-                mTimeoutHandler.postDelayed(mTimeoutRunnable,
-                        mMaxReconnectAdvertisementDuration.toMillis());
-                logd(TAG, "Successfully started advertising for device " + deviceId + ".");
-            }
-        };
-        mBlePeripheralManager.unregisterCallback(mAssociationPeripheralCallback);
-        mBlePeripheralManager.registerCallback(mReconnectPeripheralCallback);
-        mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
-        byte[] advertiseData = createReconnectData(mReconnectDeviceId);
-        if (advertiseData == null) {
-            loge(TAG, "Unable to create advertisement data. Aborting reconnect.");
-            return;
-        }
-        startAdvertising(mReconnectServiceUuid, mAdvertiseCallback, advertiseData,
-                mReconnectDataUuid, /* scanResponse= */ null, /* scanResponseUuid= */ null);
-    }
-
-    /**
-     * Create data for reconnection advertisement.
-     *
-     * <p></p><p>Process:</p>
-     * <ol>
-     * <li>Generate random {@value SALT_BYTES} byte salt and zero-pad to
-     * {@value TOTAL_AD_DATA_BYTES} bytes.
-     * <li>Hash with stored challenge secret and truncate to {@value TRUNCATED_BYTES} bytes.
-     * <li>Concatenate hashed {@value TRUNCATED_BYTES} bytes with salt and return.
-     * </ol>
-     */
-    @Nullable
-    private byte[] createReconnectData(String deviceId) {
-        byte[] salt = ByteUtils.randomBytes(SALT_BYTES);
-        byte[] zeroPadded = ByteUtils.concatByteArrays(salt,
-                new byte[TOTAL_AD_DATA_BYTES - SALT_BYTES]);
-        mReconnectChallenge = mStorage.hashWithChallengeSecret(deviceId, zeroPadded);
-        if (mReconnectChallenge == null) {
-            return null;
-        }
-        return ByteUtils.concatByteArrays(Arrays.copyOf(mReconnectChallenge, TRUNCATED_BYTES),
-                salt);
-
-    }
-
-    @Override
-    public void startAssociation(@NonNull String nameForAssociation,
-            @NonNull AssociationCallback callback) {
-        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-        if (adapter == null) {
-            loge(TAG, "Bluetooth is unavailable on this device. Unable to start associating.");
-            return;
-        }
-
-        reset();
-        mAssociationCallback = callback;
-        mBlePeripheralManager.unregisterCallback(mReconnectPeripheralCallback);
-        mBlePeripheralManager.registerCallback(mAssociationPeripheralCallback);
-        mAdvertiseCallback = new AdvertiseCallback() {
-            @Override
-            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
-                super.onStartSuccess(settingsInEffect);
-                callback.onAssociationStartSuccess(nameForAssociation);
-                logd(TAG, "Successfully started advertising for association.");
-            }
-
-            @Override
-            public void onStartFailure(int errorCode) {
-                super.onStartFailure(errorCode);
-                callback.onAssociationStartFailure();
-                logd(TAG, "Failed to start advertising for association. Error code: " + errorCode);
-            }
-        };
-        startAdvertising(mAssociationServiceUuid, mAdvertiseCallback, /* advertiseData= */null,
-                /* advertiseDataUuid= */ null, nameForAssociation.getBytes(), mReconnectDataUuid);
-    }
-
-    /** Start the association with a new device using out of band verification code exchange */
-    @Override
-    public void startOutOfBandAssociation(
-            @NonNull String nameForAssociation,
-            @NonNull OobChannel oobChannel,
-            @NonNull AssociationCallback callback) {
-
-        logd(TAG, "Starting out of band association.");
-        startAssociation(nameForAssociation, new AssociationCallback() {
-            @Override
-            public void onAssociationStartSuccess(String deviceName) {
-                mAssociationCallback = callback;
-                boolean success = mOobConnectionManager.startOobExchange(oobChannel);
-                if (!success) {
-                    callback.onAssociationStartFailure();
-                    return;
-                }
-                callback.onAssociationStartSuccess(deviceName);
-            }
-
-            @Override
-            public void onAssociationStartFailure() {
-                callback.onAssociationStartFailure();
-            }
-        });
-        mOobConnectionManager = new OobConnectionManager();
-    }
-
-    private void startAdvertising(@NonNull UUID serviceUuid, @NonNull AdvertiseCallback callback,
-            @Nullable byte[] advertiseData,
-            @Nullable UUID advertiseDataUuid, @Nullable byte[] scanResponse,
-            @Nullable UUID scanResponseUuid) {
-        BluetoothGattService gattService = new BluetoothGattService(serviceUuid,
-                BluetoothGattService.SERVICE_TYPE_PRIMARY);
-        gattService.addCharacteristic(mWriteCharacteristic);
-        gattService.addCharacteristic(mReadCharacteristic);
-
-        AdvertiseData.Builder advertisementBuilder =
-                new AdvertiseData.Builder();
-        ParcelUuid uuid = new ParcelUuid(serviceUuid);
-        advertisementBuilder.addServiceUuid(uuid);
-        if (advertiseData != null) {
-            ParcelUuid dataUuid = uuid;
-            if (advertiseDataUuid != null) {
-                dataUuid = new ParcelUuid(advertiseDataUuid);
-            }
-            advertisementBuilder.addServiceData(dataUuid, advertiseData);
-        }
-
-        AdvertiseData.Builder scanResponseBuilder =
-                new AdvertiseData.Builder();
-        if (scanResponse != null && scanResponseUuid != null) {
-            ParcelUuid scanResponseParcelUuid = new ParcelUuid(scanResponseUuid);
-            scanResponseBuilder.addServiceData(scanResponseParcelUuid, scanResponse);
-        }
-
-        mBlePeripheralManager.startAdvertising(gattService, advertisementBuilder.build(),
-                scanResponseBuilder.build(), callback);
-    }
-
-    private void addConnectedDevice(BluetoothDevice device, boolean isReconnect) {
-        addConnectedDevice(device, isReconnect, /* oobConnectionManager= */ null);
-    }
-
-    private void addConnectedDevice(@NonNull BluetoothDevice device, boolean isReconnect,
-            @Nullable OobConnectionManager oobConnectionManager) {
-        EventLog.onDeviceConnected();
-        mBlePeripheralManager.stopAdvertising(mAdvertiseCallback);
-        if (mTimeoutHandler != null) {
-            mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
-        }
-
-        if (device.getName() == null) {
-            logd(TAG, "Device connected, but name is null; issuing request to retrieve device "
-                    + "name.");
-            mBlePeripheralManager.retrieveDeviceName(device);
-        } else {
-            setClientDeviceName(device.getName());
-        }
-        setClientDeviceAddress(device.getAddress());
-
-        BleDeviceMessageStream secureStream = new BleDeviceMessageStream(mBlePeripheralManager,
-                device, mWriteCharacteristic, mReadCharacteristic,
-                mDefaultMtuSize - ATT_PROTOCOL_BYTES);
-        secureStream.setMessageReceivedErrorListener(
-                exception -> {
-                    disconnectWithError("Error occurred in stream: " + exception.getMessage());
-                });
-        SecureChannel secureChannel;
-        if (isReconnect) {
-            secureChannel = new ReconnectSecureChannel(secureStream, mStorage, mReconnectDeviceId,
-                    mReconnectChallenge);
-        } else if (oobConnectionManager != null) {
-            secureChannel = new OobAssociationSecureChannel(secureStream, mStorage,
-                    oobConnectionManager);
-        } else {
-            secureChannel = new AssociationSecureChannel(secureStream, mStorage);
-        }
-        secureChannel.registerCallback(mSecureChannelCallback);
-        ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null);
-        connectedDevice.mSecureChannel = secureChannel;
-        addConnectedDevice(connectedDevice);
-        if (isReconnect) {
-            setDeviceIdAndNotifyCallbacks(mReconnectDeviceId);
-            mReconnectDeviceId = null;
-            mReconnectChallenge = null;
-        }
-    }
-
-    private void setMtuSize(int mtuSize) {
-        ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-        if (connectedDevice != null
-                && connectedDevice.mSecureChannel != null
-                && connectedDevice.mSecureChannel.getStream() != null) {
-            ((BleDeviceMessageStream) connectedDevice.mSecureChannel.getStream())
-                    .setMaxWriteSize(mtuSize - ATT_PROTOCOL_BYTES);
-        }
-    }
-
-    private final BlePeripheralManager.Callback mReconnectPeripheralCallback =
-            new BlePeripheralManager.Callback() {
-
-                @Override
-                public void onDeviceNameRetrieved(String deviceName) {
-                    // Ignored.
-                }
-
-                @Override
-                public void onMtuSizeChanged(int size) {
-                    setMtuSize(size);
-                }
-
-                @Override
-                public void onRemoteDeviceConnected(BluetoothDevice device) {
-                    addConnectedDevice(device, /* isReconnect= */ true);
-                }
-
-                @Override
-                public void onRemoteDeviceDisconnected(BluetoothDevice device) {
-                    String deviceId = mReconnectDeviceId;
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
-                    // Reset before invoking callbacks to avoid a race condition with reconnect
-                    // logic.
-                    reset();
-                    if (connectedDevice != null) {
-                        deviceId = connectedDevice.mDeviceId;
-                    }
-                    final String finalDeviceId = deviceId;
-                    if (finalDeviceId == null) {
-                        logw(TAG, "Callbacks were not issued for disconnect because the device id "
-                                + "was null.");
-                        return;
-                    }
-                    logd(TAG, "Connected device " + finalDeviceId + " disconnected.");
-                    mCallbacks.invoke(callback -> callback.onDeviceDisconnected(finalDeviceId));
-                }
-            };
-
-    private final BlePeripheralManager.Callback mAssociationPeripheralCallback =
-            new BlePeripheralManager.Callback() {
-                @Override
-                public void onDeviceNameRetrieved(String deviceName) {
-                    if (deviceName == null) {
-                        return;
-                    }
-                    setClientDeviceName(deviceName);
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-                    if (connectedDevice == null || connectedDevice.mDeviceId == null) {
-                        return;
-                    }
-                    mStorage.updateAssociatedDeviceName(connectedDevice.mDeviceId, deviceName);
-                }
-
-                @Override
-                public void onMtuSizeChanged(int size) {
-                    setMtuSize(size);
-                }
-
-                @Override
-                public void onRemoteDeviceConnected(BluetoothDevice device) {
-                    addConnectedDevice(device, /* isReconnect= */ false, mOobConnectionManager);
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-                    if (connectedDevice == null || connectedDevice.mSecureChannel == null) {
-                        return;
-                    }
-                    ((AssociationSecureChannel) connectedDevice.mSecureChannel)
-                            .setShowVerificationCodeListener(
-                                    code -> {
-                                        if (!isAssociating()) {
-                                            loge(TAG, "No valid callback for association.");
-                                            return;
-                                        }
-                                        mAssociationCallback.onVerificationCodeAvailable(code);
-                                    });
-                }
-
-                @Override
-                public void onRemoteDeviceDisconnected(BluetoothDevice device) {
-                    logd(TAG, "Remote device disconnected.");
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
-                    if (isAssociating()) {
-                        mAssociationCallback.onAssociationError(
-                                DEVICE_ERROR_UNEXPECTED_DISCONNECTION);
-                    }
-                    // Reset before invoking callbacks to avoid a race condition with reconnect
-                    // logic.
-                    reset();
-                    if (connectedDevice == null || connectedDevice.mDeviceId == null) {
-                        logw(TAG, "Callbacks were not issued for disconnect.");
-                        return;
-                    }
-                    mCallbacks.invoke(callback -> callback.onDeviceDisconnected(
-                            connectedDevice.mDeviceId));
-                }
-            };
-
-    private final Runnable mTimeoutRunnable = new Runnable() {
-        @Override
-        public void run() {
-            logd(TAG, "Timeout period expired without a connection. Restarting advertisement.");
-            mBlePeripheralManager.stopAdvertising(mAdvertiseCallback);
-            connectToDevice(UUID.fromString(mReconnectDeviceId));
-        }
-    };
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/AcceptTask.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/AcceptTask.java
deleted file mode 100644
index 2ada256..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/AcceptTask.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothServerSocket;
-import android.bluetooth.BluetoothSocket;
-
-import androidx.annotation.Nullable;
-
-import java.io.IOException;
-import java.util.UUID;
-
-/**
- * This task runs while listening for incoming connections. It behaves like a server. It runs until
- * a connection is accepted (or until cancelled).
- */
-public class AcceptTask implements Runnable {
-    private static final String TAG = "AcceptTask";
-    private static final String SERVICE_NAME_SECURE = "NAME_SECURE";
-    private static final String SERVICE_NAME_INSECURE = "NAME_INSECURE";
-    private final UUID mServiceUuid;
-    private final boolean mIsSecure;
-    private final OnTaskCompletedListener mListener;
-    private final BluetoothAdapter mAdapter;
-    private BluetoothServerSocket mServerSocket;
-
-    AcceptTask(BluetoothAdapter adapter, boolean isSecure, UUID serviceUuid,
-            OnTaskCompletedListener listener) {
-        mListener = listener;
-        mAdapter = adapter;
-        mServiceUuid = serviceUuid;
-        mIsSecure = isSecure;
-    }
-
-    /**
-     * Start the socket to listen to any incoming connection request.
-     *
-     * @return {@code true} if listening is started successfully.
-     */
-    boolean startListening() {
-        // Create a new listening server socket
-        try {
-            if (mIsSecure) {
-                mServerSocket = mAdapter.listenUsingRfcommWithServiceRecord(SERVICE_NAME_SECURE,
-                        mServiceUuid);
-            } else {
-                mServerSocket = mAdapter.listenUsingInsecureRfcommWithServiceRecord(
-                        SERVICE_NAME_INSECURE, mServiceUuid);
-            }
-        } catch (IOException e) {
-            loge(TAG, "Socket listen() failed", e);
-            return false;
-        }
-        return true;
-    }
-
-    @Override
-    public void run() {
-        logd(TAG, "BEGIN AcceptTask: " + this);
-        BluetoothSocket socket = null;
-
-        // Listen to the server socket if we're not connected
-        while (true) {
-            try {
-                socket = mServerSocket.accept();
-            } catch (IOException e) {
-                loge(TAG, "accept() failed", e);
-                break;
-            }
-            if (socket != null) {
-                break;
-            }
-        }
-
-        mListener.onTaskCompleted(socket, mIsSecure);
-    }
-
-    void cancel() {
-        logd(TAG, "CANCEL AcceptTask: " + this);
-        try {
-            if (mServerSocket != null) {
-                mServerSocket.close();
-            }
-        } catch (IOException e) {
-            loge(TAG, "close() of server failed", e);
-        }
-    }
-
-    interface OnTaskCompletedListener {
-        /**
-         * Will be called when the accept task is completed.
-         *
-         * @param socket   will be {@code null} if the task failed.
-         * @param isSecure is {@code true} when it is listening to a secure RFCOMM channel.
-         */
-        void onTaskCompleted(@Nullable BluetoothSocket socket, boolean isSecure);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/CarSppManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/CarSppManager.java
deleted file mode 100644
index e9f0cc1..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/CarSppManager.java
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_UNEXPECTED_DISCONNECTION;
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.AssociationSecureChannel;
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.connection.DeviceMessageStream;
-import com.android.car.connecteddevice.connection.ReconnectSecureChannel;
-import com.android.car.connecteddevice.connection.SecureChannel;
-import com.android.car.connecteddevice.oob.OobChannel;
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.EventLog;
-
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-
-/**
- * Communication manager that allows for targeted connections to a specific device from the car
- * using {@link SppManager} .
- */
-public class CarSppManager extends CarBluetoothManager {
-
-    private static final String TAG = "CarSppManager";
-
-    private final SppManager mSppManager;
-
-    private final UUID mAssociationServiceUuid;
-
-    private final int mPacketMaxBytes;
-
-    private String mReconnectDeviceId;
-
-    private OobConnectionManager mOobConnectionManager;
-
-    private Executor mCallbackExecutor;
-
-    private AssociationCallback mAssociationCallback;
-
-    /**
-     * Initialize a new instance of manager.
-     *
-     * @param sppManager             {@link SppManager} for establishing connection.
-     * @param connectedDeviceStorage Shared {@link ConnectedDeviceStorage} for companion features.
-     * @param packetMaxBytes         Maximum size in bytes to write in one packet.
-     */
-    public CarSppManager(@NonNull SppManager sppManager,
-            @NonNull ConnectedDeviceStorage connectedDeviceStorage,
-            @NonNull UUID associationServiceUuid,
-            int packetMaxBytes) {
-        super(connectedDeviceStorage);
-        mSppManager = sppManager;
-        mCallbackExecutor = Executors.newSingleThreadExecutor();
-        mAssociationServiceUuid = associationServiceUuid;
-        mPacketMaxBytes = packetMaxBytes;
-    }
-
-    @Override
-    public void stop() {
-        super.stop();
-        reset();
-    }
-
-    @Override
-    public void disconnectDevice(@NonNull String deviceId) {
-        ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-        if (connectedDevice == null || !deviceId.equals(connectedDevice.mDeviceId)) {
-            return;
-        }
-        reset();
-    }
-
-    @Override
-    public AssociationCallback getAssociationCallback() {
-        return mAssociationCallback;
-    }
-
-    @Override
-    public void setAssociationCallback(AssociationCallback callback) {
-        mAssociationCallback = callback;
-    }
-
-    @Override
-    public void initiateConnectionToDevice(@NonNull UUID deviceId) {
-        logd(TAG, "Start spp reconnection listening for device with id: " + deviceId.toString());
-        mReconnectDeviceId = deviceId.toString();
-        mSppManager.unregisterCallback(mAssociationSppCallback);
-        mSppManager.registerCallback(mReconnectSppCallback, mCallbackExecutor);
-        mSppManager.startListening(deviceId);
-    }
-
-    @Override
-    public void reset() {
-        super.reset();
-        mReconnectDeviceId = null;
-        mAssociationCallback = null;
-        mSppManager.cleanup();
-    }
-
-    /**
-     * Start the association by listening to incoming connect request.
-     */
-    @Override
-    public void startAssociation(@NonNull String nameForAssociation,
-            @NonNull AssociationCallback callback) {
-        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
-        if (adapter == null) {
-            loge(TAG, "Bluetooth is unavailable on this device. Unable to start associating.");
-            return;
-        }
-
-        reset();
-        mAssociationCallback = callback;
-        mSppManager.unregisterCallback(mReconnectSppCallback);
-        mSppManager.registerCallback(mAssociationSppCallback, mCallbackExecutor);
-        if (mSppManager.startListening(mAssociationServiceUuid)) {
-            callback.onAssociationStartSuccess(/* deviceName= */ null);
-        } else {
-            callback.onAssociationStartFailure();
-        }
-    }
-
-    /**
-     * Start the association with a new device using out of band verification code exchange
-     */
-    @Override
-    public void startOutOfBandAssociation(@NonNull String nameForAssociation,
-            @NonNull OobChannel oobChannel,
-            @NonNull AssociationCallback callback) {
-
-        logd(TAG, "Starting out of band association.");
-        startAssociation(nameForAssociation, new AssociationCallback() {
-            @Override
-            public void onAssociationStartSuccess(String deviceName) {
-                mAssociationCallback = callback;
-                boolean success = mOobConnectionManager.startOobExchange(oobChannel);
-                if (!success) {
-                    callback.onAssociationStartFailure();
-                    return;
-                }
-                callback.onAssociationStartSuccess(deviceName);
-            }
-
-            @Override
-            public void onAssociationStartFailure() {
-                callback.onAssociationStartFailure();
-            }
-        });
-        mOobConnectionManager = new OobConnectionManager();
-    }
-
-    private void onDeviceConnected(BluetoothDevice device, boolean isReconnect) {
-        onDeviceConnected(device, isReconnect, /* isOob= */ false);
-    }
-
-    private void onDeviceConnected(BluetoothDevice device, boolean isReconnect, boolean isOob) {
-        EventLog.onDeviceConnected();
-        setClientDeviceAddress(device.getAddress());
-        setClientDeviceName(device.getName());
-        DeviceMessageStream secureStream = new SppDeviceMessageStream(mSppManager, device,
-                mPacketMaxBytes);
-        secureStream.setMessageReceivedErrorListener(
-                exception -> {
-                    disconnectWithError("Error occurred in stream: " + exception.getMessage(),
-                            exception);
-                });
-        SecureChannel secureChannel;
-        // TODO(b/157492943): Define an out of band version of ReconnectSecureChannel
-        if (isReconnect) {
-            secureChannel = new ReconnectSecureChannel(secureStream, mStorage, mReconnectDeviceId,
-                    /* expectedChallengeResponse= */ null);
-        } else if (isOob) {
-            // TODO(b/160901821): Integrate Oob with Spp channel
-            loge(TAG, "Oob verification is currently not available for Spp");
-            return;
-        } else {
-            secureChannel = new AssociationSecureChannel(secureStream, mStorage);
-        }
-        secureChannel.registerCallback(mSecureChannelCallback);
-        ConnectedRemoteDevice connectedDevice = new ConnectedRemoteDevice(device, /* gatt= */ null);
-        connectedDevice.mSecureChannel = secureChannel;
-        addConnectedDevice(connectedDevice);
-        if (isReconnect) {
-            setDeviceIdAndNotifyCallbacks(mReconnectDeviceId);
-            mReconnectDeviceId = null;
-        }
-    }
-
-    private final SppManager.ConnectionCallback mReconnectSppCallback =
-            new SppManager.ConnectionCallback() {
-                @Override
-                public void onRemoteDeviceConnected(BluetoothDevice device) {
-                    onDeviceConnected(device, /* isReconnect= */ true);
-                }
-
-                @Override
-                public void onRemoteDeviceDisconnected(BluetoothDevice device) {
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
-                    // Reset before invoking callbacks to avoid a race condition with reconnect
-                    // logic.
-                    reset();
-                    String deviceId = connectedDevice == null ? mReconnectDeviceId
-                            : connectedDevice.mDeviceId;
-                    if (deviceId != null) {
-                        logd(TAG, "Connected device " + deviceId + " disconnected.");
-                        mCallbacks.invoke(callback -> callback.onDeviceDisconnected(deviceId));
-                    }
-                }
-            };
-
-    private final SppManager.ConnectionCallback mAssociationSppCallback =
-            new SppManager.ConnectionCallback() {
-                @Override
-                public void onRemoteDeviceConnected(BluetoothDevice device) {
-                    onDeviceConnected(device, /* isReconnect= */ false);
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice();
-                    if (connectedDevice == null || connectedDevice.mSecureChannel == null) {
-                        loge(TAG,
-                                "No connected device or secure channel found when try to "
-                                        + "associate.");
-                        return;
-                    }
-                    ((AssociationSecureChannel) connectedDevice.mSecureChannel)
-                            .setShowVerificationCodeListener(
-                                    code -> {
-                                        if (mAssociationCallback == null) {
-                                            loge(TAG, "No valid callback for association.");
-                                            return;
-                                        }
-                                        mAssociationCallback.onVerificationCodeAvailable(code);
-                                    });
-                }
-
-                @Override
-                public void onRemoteDeviceDisconnected(BluetoothDevice device) {
-                    ConnectedRemoteDevice connectedDevice = getConnectedDevice(device);
-                    if (isAssociating()) {
-                        mAssociationCallback.onAssociationError(
-                                DEVICE_ERROR_UNEXPECTED_DISCONNECTION);
-                    }
-                    // Reset before invoking callbacks to avoid a race condition with reconnect
-                    // logic.
-                    reset();
-                    if (connectedDevice != null && connectedDevice.mDeviceId != null) {
-                        mCallbacks.invoke(callback -> callback.onDeviceDisconnected(
-                                connectedDevice.mDeviceId));
-                    }
-                }
-            };
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/ConnectedTask.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/ConnectedTask.java
deleted file mode 100644
index 2318e69..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/ConnectedTask.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logi;
-
-import android.bluetooth.BluetoothSocket;
-
-import androidx.annotation.NonNull;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * This task runs during a connection with a remote device. It handles all incoming and outgoing
- * data.
- */
-public class ConnectedTask implements Runnable {
-    private static final String TAG = "ConnectedTask";
-    private final BluetoothSocket mSocket;
-    private final InputStream mInputStream;
-    private final OutputStream mOutputStream;
-    private Callback mCallback;
-
-    ConnectedTask(@NonNull InputStream inputStream, @NonNull OutputStream outputStream,
-            @NonNull BluetoothSocket socket,
-            @NonNull Callback callback) {
-        mInputStream = inputStream;
-        mOutputStream = outputStream;
-        mSocket = socket;
-        mCallback = callback;
-    }
-
-    @Override
-    public void run() {
-        logi(TAG, "Begin ConnectedTask: started to listen to incoming messages.");
-        // Keep listening to the InputStream when task started.
-        while (true) {
-            try {
-                int dataLength = mInputStream.available();
-                if (dataLength == 0) {
-                    continue;
-                }
-                byte[] buffer = new byte[dataLength];
-                // Read from the InputStream
-                mInputStream.read(buffer);
-                mCallback.onMessageReceived(buffer);
-                logd(TAG, "received raw bytes from remote device with length: " + dataLength);
-            } catch (IOException e) {
-                loge(TAG,
-                        "Encountered an exception when listening for incoming message, "
-                                + "disconnected", e);
-                mCallback.onDisconnected();
-                break;
-            }
-        }
-    }
-
-    /**
-     * Write to the connected OutputStream.
-     *
-     * @param buffer The bytes to write
-     */
-    void write(@NonNull byte[] buffer) {
-        try {
-            mOutputStream.write(buffer);
-            logd(TAG, "Sent buffer to remote device with length: " + buffer.length);
-        } catch (IOException e) {
-            loge(TAG, "Exception during write", e);
-        }
-    }
-
-    void cancel() {
-        logd(TAG, "cancel connected task: close connected socket.");
-        try {
-            mSocket.close();
-        } catch (IOException e) {
-            loge(TAG, "close() of connected socket failed", e);
-        }
-    }
-
-    interface Callback {
-        void onMessageReceived(@NonNull byte[] message);
-
-        void onDisconnected();
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStream.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStream.java
deleted file mode 100644
index d92ddca..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStream.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.BluetoothDevice;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.connection.DeviceMessageStream;
-
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * Spp message stream to a device.
- */
-class SppDeviceMessageStream extends DeviceMessageStream {
-
-    private static final String TAG = "SppDeviceMessageStream";
-
-    private final SppManager mSppManager;
-    private final BluetoothDevice mDevice;
-    private final Executor mCallbackExecutor = Executors.newSingleThreadExecutor();
-
-
-    SppDeviceMessageStream(@NonNull SppManager sppManager,
-            @NonNull BluetoothDevice device, int maxWriteSize) {
-        super(maxWriteSize);
-        mSppManager = sppManager;
-        mDevice = device;
-        mSppManager.addOnMessageReceivedListener(this::onMessageReceived, mCallbackExecutor);
-    }
-
-    @Override
-    protected void send(byte[] data) {
-        mSppManager.write(data);
-        sendCompleted();
-    }
-
-    @VisibleForTesting
-    void onMessageReceived(@NonNull BluetoothDevice device, @NonNull byte[] value) {
-        logd(TAG, "Received a message from a device (" + device.getAddress() + ").");
-        if (!mDevice.equals(device)) {
-            logw(TAG, "Received a message from a device (" + device.getAddress() + ") that is not "
-                    + "the expected device (" + mDevice.getAddress() + ") registered to this "
-                    + "stream. Ignoring.");
-            return;
-        }
-
-        onDataReceived(value);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppManager.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppManager.java
deleted file mode 100644
index 57a9c89..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppManager.java
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothSocket;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.util.ThreadSafeCallbacks;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-/**
- * A generic class that handles all the Spp connection events including:
- * <ol>
- *     <li>listen and accept connection request from client.
- *     <li>send a message through an established connection.
- *     <li>notify any connection or message events happening during the connection.
- * </ol>
- */
-public class SppManager {
-    private static final String TAG = "SppManager";
-    // Service names and UUIDs of SDP(Service Discovery Protocol) record, need to keep it consistent
-    // among client and server.
-    private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
-    private final boolean mIsSecure;
-    private Object mLock = new Object();
-    /**
-     * Task to listen to secure RFCOMM channel.
-     */
-    @VisibleForTesting
-    AcceptTask mAcceptTask;
-    /**
-     * Task to start and maintain a connection.
-     */
-    @VisibleForTesting
-    ConnectedTask mConnectedTask;
-    @VisibleForTesting
-    ExecutorService mConnectionExecutor = Executors.newSingleThreadExecutor();
-    private BluetoothDevice mDevice;
-    @GuardedBy("mLock")
-    private final SppPayloadStream mPayloadStream = new SppPayloadStream();
-    @GuardedBy("mLock")
-    @VisibleForTesting
-    ConnectionState mState;
-    private final ThreadSafeCallbacks<ConnectionCallback> mCallbacks = new ThreadSafeCallbacks<>();
-    private final ThreadSafeCallbacks<OnMessageReceivedListener> mReceivedListeners =
-            new ThreadSafeCallbacks<>();
-
-    public SppManager(@NonNull boolean isSecure) {
-        mPayloadStream.setMessageCompletedListener(this::onMessageCompleted);
-        mIsSecure = isSecure;
-    }
-
-    @VisibleForTesting
-    enum ConnectionState {
-        NONE,
-        LISTEN,
-        CONNECTING,
-        CONNECTED,
-        DISCONNECTED,
-    }
-
-    /**
-     * Registers the given callback to be notified of various events within the {@link SppManager}.
-     *
-     * @param callback The callback to be notified.
-     */
-    void registerCallback(@NonNull ConnectionCallback callback, @NonNull Executor executor) {
-        mCallbacks.add(callback, executor);
-    }
-
-    /**
-     * Unregisters a previously registered callback.
-     *
-     * @param callback The callback to unregister.
-     */
-    void unregisterCallback(@NonNull ConnectionCallback callback) {
-        mCallbacks.remove(callback);
-    }
-
-    /**
-     * Adds a listener to be notified of a write to characteristics.
-     *
-     * @param listener The listener to invoke.
-     */
-    void addOnMessageReceivedListener(@NonNull OnMessageReceivedListener listener,
-            @NonNull Executor executor) {
-        mReceivedListeners.add(listener, executor);
-    }
-
-    /**
-     * Removes the given listener from being notified of characteristic writes.
-     *
-     * @param listener The listener to remove.
-     */
-    void removeOnMessageReceivedListener(@NonNull OnMessageReceivedListener listener) {
-        mReceivedListeners.remove(listener);
-    }
-
-    /**
-     * Start listening to connection request from the client.
-     *
-     * @param serviceUuid The Uuid which the accept task is listening on.
-     * @return {@code true} if listening is started successfully
-     */
-    boolean startListening(@NonNull UUID serviceUuid) {
-        logd(TAG, "Start socket to listening to incoming connection request.");
-        if (mConnectedTask != null) {
-            mConnectedTask.cancel();
-            mConnectedTask = null;
-        }
-
-        // Start the task to listen on a BluetoothServerSocket
-        if (mAcceptTask != null) {
-            mAcceptTask.cancel();
-        }
-        mAcceptTask = new AcceptTask(mAdapter, mIsSecure, serviceUuid, mAcceptTaskListener);
-        if (!mAcceptTask.startListening()) {
-            // TODO(b/159376003): Handle listening error.
-            mAcceptTask.cancel();
-            mAcceptTask = null;
-            return false;
-        }
-        synchronized (mLock) {
-            mState = ConnectionState.LISTEN;
-        }
-        mConnectionExecutor.execute(mAcceptTask);
-        return true;
-    }
-
-    /**
-     * Send data to remote connected bluetooth device.
-     *
-     * @param data the raw data that wait to be sent
-     * @return {@code true} if the message is sent to client successfully.
-     */
-    boolean write(@NonNull byte[] data) {
-        ConnectedTask connectedTask;
-        // Synchronize a copy of the ConnectedTask
-        synchronized (mLock) {
-            if (mState != ConnectionState.CONNECTED) {
-                loge(TAG, "Try to send data when device is disconnected");
-                return false;
-            }
-            connectedTask = mConnectedTask;
-        }
-        byte[] dataReadyToSend = SppPayloadStream.wrapWithArrayLength(data);
-        if (dataReadyToSend == null) {
-            loge(TAG, "Wrapping data with array length failed.");
-            return false;
-        }
-        connectedTask.write(dataReadyToSend);
-        return true;
-    }
-
-    /**
-     * Cleans up the registered listeners.
-     */
-    void cleanup() {
-        // Clears all registered listeners. IHU only supports single connection.
-        mReceivedListeners.clear();
-    }
-
-    /**
-     * Start the ConnectedTask to begin and maintain a RFCOMM channel.
-     *
-     * @param socket   The BluetoothSocket on which the connection was made
-     * @param device   The BluetoothDevice that has been connected
-     * @param isSecure The type of current established channel
-     */
-    @GuardedBy("mLock")
-    private void startConnectionLocked(BluetoothSocket socket, BluetoothDevice device,
-            boolean isSecure) {
-        logd(TAG, "Get accepted bluetooth socket, start listening to incoming messages.");
-
-        // Cancel any task currently running a connection
-        if (mConnectedTask != null) {
-            mConnectedTask.cancel();
-            mConnectedTask = null;
-        }
-
-        // Cancel the accept task because we only want to connect to one device
-        if (mAcceptTask != null) {
-            mAcceptTask.cancel();
-            mAcceptTask = null;
-        }
-        logd(TAG, "Create ConnectedTask: is secure? " + isSecure);
-        InputStream inputStream;
-        OutputStream outputStream;
-        mDevice = device;
-
-        // Get the BluetoothSocket input and output streams
-        try {
-            inputStream = socket.getInputStream();
-            outputStream = socket.getOutputStream();
-        } catch (IOException e) {
-            loge(TAG, "Can not get stream from BluetoothSocket. Connection failed.", e);
-            return;
-        }
-        mState = ConnectionState.CONNECTED;
-        mCallbacks.invoke(callback -> callback.onRemoteDeviceConnected(device));
-
-        // Start the task to manage the connection and perform transmissions
-        mConnectedTask = new ConnectedTask(inputStream, outputStream, socket,
-                mConnectedTaskCallback);
-        mConnectionExecutor.execute(mConnectedTask);
-    }
-
-    private void onMessageCompleted(@NonNull byte[] message) {
-        mReceivedListeners.invoke(listener -> listener.onMessageReceived(mDevice, message));
-    }
-
-    @VisibleForTesting
-    final AcceptTask.OnTaskCompletedListener mAcceptTaskListener =
-            new AcceptTask.OnTaskCompletedListener() {
-                @Override
-                public void onTaskCompleted(BluetoothSocket socket, boolean isSecure) {
-                    if (socket == null) {
-                        loge(TAG, "AcceptTask failed getting the socket");
-                        return;
-                    }
-                    // Connection accepted
-                    synchronized (mLock) {
-                        switch (mState) {
-                            case LISTEN:
-                            case CONNECTING:
-                                startConnectionLocked(socket, socket.getRemoteDevice(), isSecure);
-                                break;
-                            case NONE:
-                                loge(TAG, "AcceptTask is done while in NONE state.");
-                                break;
-                            case CONNECTED:
-                                // Already connected. Terminate new socket.
-                                try {
-                                    socket.close();
-                                } catch (IOException e) {
-                                    loge(TAG, "Could not close unwanted socket", e);
-                                }
-                                break;
-                            case DISCONNECTED:
-                                loge(TAG, "AcceptTask is done while in DISCONNECTED state.");
-                                break;
-                        }
-                    }
-                }
-            };
-
-    @VisibleForTesting
-    final ConnectedTask.Callback mConnectedTaskCallback = new ConnectedTask.Callback() {
-        @Override
-        public void onMessageReceived(byte[] message) {
-            synchronized (mLock) {
-                try {
-                    mPayloadStream.write(message);
-                } catch (IOException e) {
-                    loge(TAG, "Error writes message to spp payload stream: " + e.getMessage());
-                }
-            }
-        }
-
-        @Override
-        public void onDisconnected() {
-            synchronized (mLock) {
-                mState = ConnectionState.DISCONNECTED;
-                mCallbacks.invoke(callback -> callback.onRemoteDeviceDisconnected(mDevice));
-            }
-        }
-    };
-
-    /**
-     * Interface to be notified of various events within the {@link SppManager}.
-     */
-    interface ConnectionCallback {
-
-        /**
-         * Triggered when a bluetooth device connected.
-         *
-         * @param device Remote device that connected on Spp.
-         */
-        void onRemoteDeviceConnected(@NonNull BluetoothDevice device);
-
-        /**
-         * Triggered when a bluetooth device disconnected.
-         *
-         * @param device Remote device that disconnected on Spp.
-         */
-        void onRemoteDeviceDisconnected(@NonNull BluetoothDevice device);
-    }
-
-    /**
-     * An interface for classes that wish to be notified of incoming messages.
-     */
-    interface OnMessageReceivedListener {
-        /**
-         * Triggered when this SppManager receives a write request from a remote device.
-         *
-         * @param device The bluetooth device that sending the message.
-         * @param value  The value that was written.
-         */
-        void onMessageReceived(@NonNull BluetoothDevice device, @NonNull byte[] value);
-    }
-
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppPayloadStream.java b/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppPayloadStream.java
deleted file mode 100644
index f05a76f..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/connection/spp/SppPayloadStream.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.Arrays;
-
-/**
- * For spp stream will be segmented to several parts, a completed message length need to be prepend
- * to any message sent. This class will take care of the decode and encode of the incoming and
- * outgoing message.
- */
-class SppPayloadStream {
-    private static final String TAG = "SppPayloadStream";
-    // An int will take 4 bytes.
-    private static final int LENGTH_BYTES_SIZE = 4;
-    private final ByteArrayOutputStream mPendingStream = new ByteArrayOutputStream();
-    private OnMessageCompletedListener mOnMessageCompletedListener;
-    private int mCurrentMessageTotalLength;
-
-    /**
-     * Writes data to the {@code pendingStream}, inform the {@code messageCompletedListener} when
-     * the message is completed, otherwise store the data into {@code pendingStream} and waiting for
-     * the following parts.
-     *
-     * @param data Received byte array
-     * @throws IOException If there are some errors writing data to {@code pendingStream}
-     */
-    public void write(@NonNull byte[] data) throws IOException {
-        if (mPendingStream.size() == 0) {
-            int currentLength = data.length;
-            // Arbitrarily choose a byte order, need to use the same byte order for server and
-            // client.
-            mCurrentMessageTotalLength = ByteBuffer.wrap(
-                    Arrays.copyOf(data, LENGTH_BYTES_SIZE)).order(
-                    ByteOrder.LITTLE_ENDIAN).getInt();
-            byte[] payload = Arrays.copyOfRange(data, LENGTH_BYTES_SIZE, currentLength);
-            mPendingStream.write(payload);
-        } else {
-            mPendingStream.write(data);
-        }
-
-        if (mPendingStream.size() > mCurrentMessageTotalLength) {
-            // TODO(b/159712861): Handle this situation, e.g. disconnect.
-            loge(TAG, "Received invalid message: " + mPendingStream.toByteArray());
-            return;
-        }
-
-        if (mPendingStream.size() < mCurrentMessageTotalLength) {
-            return;
-        }
-        if (mOnMessageCompletedListener != null) {
-            mOnMessageCompletedListener.onMessageCompleted(mPendingStream.toByteArray());
-        }
-        mPendingStream.reset();
-
-    }
-
-    /**
-     * Register the given listener to be notified when a completed message is received.
-     *
-     * @param listener The listener to be notified
-     */
-    public void setMessageCompletedListener(@Nullable OnMessageCompletedListener listener) {
-        mOnMessageCompletedListener = listener;
-    }
-
-    /**
-     * Wrap the raw byte array with array length.
-     * <p>
-     * Should be called every time when server wants to send a message to client.
-     *
-     * @param rawData Original data
-     * @return The wrapped data
-     * @throws IOException If there are some errors writing data to {@code outputStream}
-     */
-    @Nullable
-    public static byte[] wrapWithArrayLength(@NonNull byte[] rawData) {
-        int length = rawData.length;
-        byte[] lengthBytes = ByteBuffer.allocate(LENGTH_BYTES_SIZE).order(
-                ByteOrder.LITTLE_ENDIAN).putInt(length).array();
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        try {
-            outputStream.write(lengthBytes);
-            outputStream.write(rawData);
-        } catch (IOException e) {
-            loge(TAG, "Error wrap data with array length");
-            return null;
-        }
-        return outputStream.toByteArray();
-    }
-
-    /**
-     * Interface to be notified when a completed message has been received.
-     */
-    interface OnMessageCompletedListener {
-        void onMessageCompleted(@NonNull byte[] message);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/model/AssociatedDevice.java b/connected-device-lib/src/com/android/car/connecteddevice/model/AssociatedDevice.java
deleted file mode 100644
index 8622617..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/model/AssociatedDevice.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.model;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Objects;
-
-/**
- * Contains basic info of an associated device.
- */
-public class AssociatedDevice {
-
-    private final String mDeviceId;
-
-    private final String mDeviceAddress;
-
-    private final String mDeviceName;
-
-    private final boolean mIsConnectionEnabled;
-
-
-    /**
-     * Create a new AssociatedDevice.
-     *
-     * @param deviceId Id of the associated device.
-     * @param deviceAddress Address of the associated device.
-     * @param deviceName Name of the associated device. {@code null} if not known.
-     * @param isConnectionEnabled If connection is enabled for this device.
-     */
-    public AssociatedDevice(@NonNull String deviceId, @NonNull String deviceAddress,
-            @Nullable String deviceName, boolean isConnectionEnabled) {
-        mDeviceId = deviceId;
-        mDeviceAddress = deviceAddress;
-        mDeviceName = deviceName;
-        mIsConnectionEnabled = isConnectionEnabled;
-    }
-
-    /** Returns the id for this device. */
-    @NonNull
-    public String getDeviceId() {
-        return mDeviceId;
-    }
-
-    /** Returns the address for this device. */
-    @NonNull
-    public String getDeviceAddress() {
-        return mDeviceAddress;
-    }
-
-    /** Returns the name for this device or {@code null} if not known. */
-    @Nullable
-    public String getDeviceName() {
-        return mDeviceName;
-    }
-
-    /** Return if connection is enabled for this device. */
-    public boolean isConnectionEnabled() {
-        return mIsConnectionEnabled;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == this) {
-            return true;
-        }
-        if (!(obj instanceof AssociatedDevice)) {
-            return false;
-        }
-        AssociatedDevice associatedDevice = (AssociatedDevice) obj;
-        return Objects.equals(mDeviceId, associatedDevice.mDeviceId)
-                && Objects.equals(mDeviceAddress, associatedDevice.mDeviceAddress)
-                && Objects.equals(mDeviceName, associatedDevice.mDeviceName)
-                && mIsConnectionEnabled == associatedDevice.mIsConnectionEnabled;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mDeviceId, mDeviceAddress, mDeviceName, mIsConnectionEnabled);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/model/ConnectedDevice.java b/connected-device-lib/src/com/android/car/connecteddevice/model/ConnectedDevice.java
deleted file mode 100644
index 9e49a37..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/model/ConnectedDevice.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.model;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Objects;
-
-/**
- * View model representing a connected device.
- */
-public class ConnectedDevice {
-
-    private final String mDeviceId;
-
-    private final String mDeviceName;
-
-    private final boolean mBelongsToActiveUser;
-
-    private final boolean mHasSecureChannel;
-
-    /**
-     * Create a new connected device.
-     *
-     * @param deviceId Id of the connected device.
-     * @param deviceName Name of the connected device. {@code null} if not known.
-     * @param belongsToActiveUser User associated with this device is currently in the foreground.
-     * @param hasSecureChannel {@code true} if a secure channel is available for this device.
-     */
-    public ConnectedDevice(@NonNull String deviceId, @Nullable String deviceName,
-            boolean belongsToActiveUser, boolean hasSecureChannel) {
-        mDeviceId = deviceId;
-        mDeviceName = deviceName;
-        mBelongsToActiveUser = belongsToActiveUser;
-        mHasSecureChannel = hasSecureChannel;
-    }
-
-    /** Returns the id for this device. */
-    @NonNull
-    public String getDeviceId() {
-        return mDeviceId;
-    }
-
-    /** Returns the name for this device or {@code null} if not known. */
-    @Nullable
-    public String getDeviceName() {
-        return mDeviceName;
-    }
-
-    /**
-     * Returns {@code true} if this device is associated with the user currently in the foreground.
-     */
-    public boolean isAssociatedWithActiveUser() {
-        return mBelongsToActiveUser;
-    }
-
-    /** Returns {@code true} if this device has a secure channel available. */
-    public boolean hasSecureChannel() {
-        return mHasSecureChannel;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == this) {
-            return true;
-        }
-        if (!(obj instanceof ConnectedDevice)) {
-            return false;
-        }
-        ConnectedDevice connectedDevice = (ConnectedDevice) obj;
-        return Objects.equals(mDeviceId, connectedDevice.mDeviceId)
-                && Objects.equals(mDeviceName, connectedDevice.mDeviceName)
-                && mBelongsToActiveUser == connectedDevice.mBelongsToActiveUser
-                && mHasSecureChannel == connectedDevice.mHasSecureChannel;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mDeviceId, mDeviceName, mBelongsToActiveUser, mHasSecureChannel);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/model/OobEligibleDevice.java b/connected-device-lib/src/com/android/car/connecteddevice/model/OobEligibleDevice.java
deleted file mode 100644
index 6e910d3..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/model/OobEligibleDevice.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.model;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-
-import java.lang.annotation.Retention;
-import java.util.Objects;
-
-/** Device that may be used for an out-of-band channel. */
-public class OobEligibleDevice {
-
-    @Retention(SOURCE)
-    @IntDef(value = { OOB_TYPE_BLUETOOTH })
-    public @interface OobType {}
-    public static final int OOB_TYPE_BLUETOOTH = 0;
-
-    private final String mDeviceAddress;
-
-    @OobType
-    private final int mOobType;
-
-    public OobEligibleDevice(@NonNull String deviceAddress, @OobType int oobType) {
-        mDeviceAddress = deviceAddress;
-        mOobType = oobType;
-    }
-
-    @NonNull
-    public String getDeviceAddress() {
-        return mDeviceAddress;
-    }
-
-    @OobType
-    public int getOobType() {
-        return mOobType;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == this) {
-            return true;
-        }
-        if (!(obj instanceof OobEligibleDevice)) {
-            return false;
-        }
-        OobEligibleDevice device = (OobEligibleDevice) obj;
-        return Objects.equals(device.mDeviceAddress, mDeviceAddress)
-                && device.mOobType == mOobType;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mDeviceAddress, mOobType);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannel.java
deleted file mode 100644
index 1325f11..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannel.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.oob;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothSocket;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.time.Duration;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Handles out of band data exchange over a secure RFCOMM channel.
- */
-public class BluetoothRfcommChannel implements OobChannel {
-    private static final String TAG = "BluetoothRfcommChannel";
-    // TODO (b/159500330) Generate random UUID.
-    private static final UUID RFCOMM_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
-    private static final Duration CONNECT_RETRY_WAIT = Duration.ofSeconds(1);
-    private BluetoothSocket mBluetoothSocket;
-    private AtomicBoolean mIsInterrupted = new AtomicBoolean();
-    @VisibleForTesting
-    Callback mCallback;
-
-    @Override
-    public void completeOobDataExchange(@NonNull OobEligibleDevice device,
-            @NonNull Callback callback) {
-        completeOobDataExchange(device, callback, BluetoothAdapter.getDefaultAdapter());
-    }
-
-    @VisibleForTesting
-    void completeOobDataExchange(OobEligibleDevice device, Callback callback,
-            BluetoothAdapter bluetoothAdapter) {
-        mCallback = callback;
-
-        BluetoothDevice remoteDevice = bluetoothAdapter.getRemoteDevice(device.getDeviceAddress());
-
-        try {
-            mBluetoothSocket = remoteDevice.createRfcommSocketToServiceRecord(RFCOMM_UUID);
-        } catch (IOException e) {
-            notifyFailure("Rfcomm socket creation with " + remoteDevice.getName() + " failed.", e);
-            return;
-        }
-
-        bluetoothAdapter.cancelDiscovery();
-
-        new Thread() {
-            @Override
-            public void run() {
-                while (!isInterrupted()) {
-                    try {
-                        mBluetoothSocket.connect();
-                        break;
-                    } catch (IOException e) {
-                        logd(TAG, "Unable to connect, trying again in "
-                                + CONNECT_RETRY_WAIT.toMillis() + " ms.");
-                    }
-                    try {
-                        Thread.sleep(CONNECT_RETRY_WAIT.toMillis());
-                    } catch (InterruptedException e) {
-                        loge(TAG, "Thread was interrupted before connection could be made.", e);
-                        Thread.currentThread().interrupt();
-                        return;
-                    }
-                }
-
-                notifySuccess();
-            }
-        }.start();
-    }
-
-    @Override
-    public void sendOobData(byte[] oobData) {
-        if (isInterrupted()) {
-            return;
-        }
-        if (mBluetoothSocket == null) {
-            notifyFailure("Bluetooth socket is null, oob data cannot be sent",
-                    /* exception= */ null);
-            return;
-        }
-        try {
-            OutputStream stream = mBluetoothSocket.getOutputStream();
-            stream.write(oobData);
-            stream.flush();
-            stream.close();
-        } catch (IOException e) {
-            notifyFailure("Sending oob data failed", e);
-        }
-    }
-
-    @Override
-    public void interrupt() {
-        logd(TAG, "Interrupt received.");
-        mIsInterrupted.set(true);
-    }
-
-    @VisibleForTesting
-    boolean isInterrupted() {
-        if (!mIsInterrupted.get()) {
-            return false;
-        }
-
-        if (mBluetoothSocket == null) {
-            return true;
-        }
-
-        try {
-            OutputStream stream = mBluetoothSocket.getOutputStream();
-            stream.flush();
-            stream.close();
-        } catch (IOException e) {
-            loge(TAG, "Unable to clean up bluetooth socket on interrupt.", e);
-        }
-
-        mBluetoothSocket = null;
-        return true;
-    }
-
-    private void notifyFailure(@NonNull String message, @Nullable Exception exception) {
-        loge(TAG, message, exception);
-        if (mCallback != null && !isInterrupted()) {
-            mCallback.onOobExchangeFailure();
-        }
-    }
-
-    private void notifySuccess() {
-        if (mCallback != null && !isInterrupted()) {
-            mCallback.onOobExchangeSuccess();
-        }
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/oob/OobChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/oob/OobChannel.java
deleted file mode 100644
index 9be7941..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/oob/OobChannel.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.oob;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-
-/**
- * An interface for handling out of band data exchange. This interface should be implemented for
- * every out of band channel that is supported in device association.
- *
- * Usage is:
- * <pre>
- *     1. Define success and failure responses in {@link Callback}
- *     2. Call {@link OobChannel#completeOobDataExchange(OobEligibleDevice, Callback)}
- * </pre>
- */
-public interface OobChannel {
-    /**
-     * Exchange out of band data with a remote device. This must be done prior to the start of the
-     * association with that device.
-     *
-     * @param device The remote device to exchange out of band data with
-     */
-    void completeOobDataExchange(@NonNull OobEligibleDevice device, @NonNull Callback callback);
-
-    /**
-     * Send raw data over the out of band channel
-     *
-     * @param oobData to be sent
-     */
-    void sendOobData(@NonNull byte[] oobData);
-
-    /** Interrupt the current data exchange and prevent callbacks from being issued. */
-    void interrupt();
-
-    /**
-     * Callbacks for {@link OobChannel#completeOobDataExchange(OobEligibleDevice, Callback)}
-     */
-    interface Callback {
-        /**
-         * Called when {@link OobChannel#completeOobDataExchange(OobEligibleDevice, Callback)}
-         * finishes successfully.
-         */
-        void onOobExchangeSuccess();
-
-        /**
-         * Called when {@link OobChannel#completeOobDataExchange(OobEligibleDevice, Callback)}
-         * fails.
-         */
-        void onOobExchangeFailure();
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/oob/OobConnectionManager.java b/connected-device-lib/src/com/android/car/connecteddevice/oob/OobConnectionManager.java
deleted file mode 100644
index f1564dd..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/oob/OobConnectionManager.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.oob;
-
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-
-import android.security.keystore.KeyProperties;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.google.common.primitives.Bytes;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * This is a class that manages a token--{@link OobConnectionManager#mEncryptionKey}-- passed via
- * an out of band {@link OobChannel} that is distinct from the channel that is currently being
- * secured.
- * <p>Intended usage:
-*  <pre>{@code
- *  OobConnectionManager oobConncetionManager = new OobConnectionManager();
- *  oobConnectionManager.startOobExchange(channel);
- *  }</pre>
- * <pre>{@code When a message is received:
- *   verificationCode = OobConnectionManager#decryptVerificationCode(byte[])
- *   check that verification code is valid
- *   if it is:
- *     encryptedMessage =  OobConnectionManager#encryptVerificationCode(byte[])
- *     send encryptedMessage
- *     verify handshake
- *   otherwise:
- *     fail handshake
- * }</pre>
- *
- * <pre>{@code
- * when oobData is received via the out of band channel:
- *   OobConnectionManager#setOobData(byte[])
- *
- * encryptedMessage = OobConnectionManager#encryptVerificationCode(byte[])
- * sendMessage
- * when a message is received:
- *   verificationCode = OobConnectionManager#decryptVerificationCode(byte[])
- *   check that verification code is valid
- *   if it is:
- *     verify handshake
- *   otherwise:
- *     fail handshake
- * }</pre>
- */
-public class OobConnectionManager {
-    private static final String TAG = "OobConnectionManager";
-    private static final String ALGORITHM = "AES/GCM/NoPadding";
-    // The nonce length is chosen to be consistent with the standard specification:
-    // Section 8.2 of https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
-    @VisibleForTesting
-    static final int NONCE_LENGTH_BYTES = 12;
-
-    private final Cipher mCipher;
-    @VisibleForTesting
-    byte[] mEncryptionIv = new byte[NONCE_LENGTH_BYTES];
-    @VisibleForTesting
-    byte[] mDecryptionIv = new byte[NONCE_LENGTH_BYTES];
-    @VisibleForTesting
-    SecretKey mEncryptionKey;
-
-    public OobConnectionManager() {
-        try {
-            mCipher = Cipher.getInstance(ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            loge(TAG, "Unable to create cipher with " + ALGORITHM + ".", e);
-            throw new RuntimeException(e);
-        }
-    }
-
-    /**
-     * Encrypts {@param verificationCode} using {@link OobConnectionManager#mEncryptionKey}
-     */
-    @NonNull
-    public byte[] encryptVerificationCode(@NonNull byte[] verificationCode)
-            throws InvalidAlgorithmParameterException,
-            BadPaddingException, InvalidKeyException, IllegalBlockSizeException {
-        mCipher.init(Cipher.ENCRYPT_MODE, mEncryptionKey, new IvParameterSpec(mEncryptionIv));
-        return mCipher.doFinal(verificationCode);
-    }
-
-    /**
-     * Decrypts {@param encryptedMessage} using {@link OobConnectionManager#mEncryptionKey}
-     */
-    @NonNull
-    public byte[] decryptVerificationCode(@NonNull byte[] encryptedMessage)
-            throws InvalidAlgorithmParameterException, BadPaddingException, InvalidKeyException,
-            IllegalBlockSizeException {
-        mCipher.init(Cipher.DECRYPT_MODE, mEncryptionKey, new IvParameterSpec(mDecryptionIv));
-        return mCipher.doFinal(encryptedMessage);
-    }
-
-    void setOobData(@NonNull byte[] oobData) {
-        mEncryptionIv = Arrays.copyOfRange(oobData, 0, NONCE_LENGTH_BYTES);
-        mDecryptionIv = Arrays.copyOfRange(oobData, NONCE_LENGTH_BYTES,
-                NONCE_LENGTH_BYTES * 2);
-        mEncryptionKey = new SecretKeySpec(
-                Arrays.copyOfRange(oobData, NONCE_LENGTH_BYTES * 2, oobData.length),
-                KeyProperties.KEY_ALGORITHM_AES);
-    }
-
-    /**
-     * Start the out of band exchange with a given {@link OobChannel}.
-     *
-     * @param oobChannel Channel to be used for exchange.
-     * @return {@code true} if exchange started successfully. {@code false} if an error occurred.
-     */
-    public boolean startOobExchange(@NonNull OobChannel oobChannel) {
-        KeyGenerator keyGenerator = null;
-        try {
-            keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
-        } catch (NoSuchAlgorithmException e) {
-            loge(TAG, "Unable to get AES key generator.", e);
-            return false;
-        }
-        mEncryptionKey = keyGenerator.generateKey();
-
-        SecureRandom secureRandom = new SecureRandom();
-        secureRandom.nextBytes(mEncryptionIv);
-        secureRandom.nextBytes(mDecryptionIv);
-
-        oobChannel.sendOobData(
-                Bytes.concat(mDecryptionIv, mEncryptionIv, mEncryptionKey.getEncoded()));
-        return true;
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceChallengeSecretEntity.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceChallengeSecretEntity.java
deleted file mode 100644
index ab802e6..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceChallengeSecretEntity.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.storage;
-
-import androidx.annotation.NonNull;
-import androidx.room.Entity;
-import androidx.room.PrimaryKey;
-
-/**
- * Table entity representing an challenge key for an associated device reconnection advertisement.
- */
-@Entity(tableName = "associated_devices_challenge_secrets")
-public class AssociatedDeviceChallengeSecretEntity {
-
-    /** Id of the device. */
-    @PrimaryKey
-    @NonNull
-    public String id;
-
-    /** Encrypted challenge key. */
-    @NonNull
-    public String encryptedChallengeSecret;
-
-    public AssociatedDeviceChallengeSecretEntity(String id, String encryptedChallengeSecret) {
-        this.id = id;
-        this.encryptedChallengeSecret = encryptedChallengeSecret;
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceDao.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceDao.java
deleted file mode 100644
index 2a1e023..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceDao.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.storage;
-
-import androidx.room.Dao;
-import androidx.room.Delete;
-import androidx.room.Insert;
-import androidx.room.OnConflictStrategy;
-import androidx.room.Query;
-
-import java.util.List;
-
-/**
- * Queries for associated device table.
- */
-@Dao
-public interface AssociatedDeviceDao {
-
-    /** Get an associated device based on device id. */
-    @Query("SELECT * FROM associated_devices WHERE id LIKE :deviceId LIMIT 1")
-    AssociatedDeviceEntity getAssociatedDevice(String deviceId);
-
-    /** Get all {@link AssociatedDeviceEntity}s associated with a user. */
-    @Query("SELECT * FROM associated_devices WHERE userId LIKE :userId")
-    List<AssociatedDeviceEntity> getAssociatedDevicesForUser(int userId);
-
-    /**
-     * Add a {@link AssociatedDeviceEntity}. Replace if a device already exists with the same
-     * device id.
-     */
-    @Insert(onConflict = OnConflictStrategy.REPLACE)
-    void addOrReplaceAssociatedDevice(AssociatedDeviceEntity associatedDevice);
-
-    /** Remove a {@link AssociatedDeviceEntity}. */
-    @Delete
-    void removeAssociatedDevice(AssociatedDeviceEntity connectedDevice);
-
-    /** Get the key associated with a device id. */
-    @Query("SELECT * FROM associated_device_keys WHERE id LIKE :deviceId LIMIT 1")
-    AssociatedDeviceKeyEntity getAssociatedDeviceKey(String deviceId);
-
-    /**
-     * Add a {@link AssociatedDeviceKeyEntity}. Replace if a device key already exists with the
-     * same device id.
-     */
-    @Insert(onConflict = OnConflictStrategy.REPLACE)
-    void addOrReplaceAssociatedDeviceKey(AssociatedDeviceKeyEntity keyEntity);
-
-    /** Remove a {@link AssociatedDeviceKeyEntity}. */
-    @Delete
-    void removeAssociatedDeviceKey(AssociatedDeviceKeyEntity keyEntity);
-
-    /** Get the challenge secret associated with a device id. */
-    @Query("SELECT * FROM associated_devices_challenge_secrets WHERE id LIKE :deviceId LIMIT 1")
-    AssociatedDeviceChallengeSecretEntity getAssociatedDeviceChallengeSecret(String deviceId);
-
-    /**
-     * Add a {@link AssociatedDeviceChallengeSecretEntity}. Replace if a secret already exists with
-     * the same device id.
-     */
-    @Insert(onConflict = OnConflictStrategy.REPLACE)
-    void addOrReplaceAssociatedDeviceChallengeSecret(
-            AssociatedDeviceChallengeSecretEntity challengeSecretEntity);
-
-    /** Remove a {@link AssociatedDeviceChallengeSecretEntity}. */
-    @Delete
-    void removeAssociatedDeviceChallengeSecret(
-            AssociatedDeviceChallengeSecretEntity challengeSecretEntity);
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceEntity.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceEntity.java
deleted file mode 100644
index 1c5182c..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceEntity.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.storage;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.room.Entity;
-import androidx.room.PrimaryKey;
-
-import com.android.car.connecteddevice.model.AssociatedDevice;
-
-/** Table entity representing an associated device. */
-@Entity(tableName = "associated_devices")
-public class AssociatedDeviceEntity {
-
-    /** Id of the device. */
-    @PrimaryKey
-    @NonNull
-    public String id;
-
-    /** Id of user associated with this device. */
-    public int userId;
-
-    /** Bluetooth address of the device. */
-    @Nullable
-    public String address;
-
-    /** Bluetooth device name. */
-    @Nullable
-    public String name;
-
-    /** {@code true} if the connection is enabled for this device.*/
-    public boolean isConnectionEnabled;
-
-    public AssociatedDeviceEntity() { }
-
-    public AssociatedDeviceEntity(int userId, AssociatedDevice associatedDevice,
-            boolean isConnectionEnabled) {
-        this.userId = userId;
-        id = associatedDevice.getDeviceId();
-        address = associatedDevice.getDeviceAddress();
-        name = associatedDevice.getDeviceName();
-        this.isConnectionEnabled = isConnectionEnabled;
-    }
-
-    /** Return a new {@link AssociatedDevice} of this entity. */
-    public AssociatedDevice toAssociatedDevice() {
-        return new AssociatedDevice(id, address, name, isConnectionEnabled);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceKeyEntity.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceKeyEntity.java
deleted file mode 100644
index 6cd791f..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceKeyEntity.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.storage;
-
-import androidx.annotation.NonNull;
-import androidx.room.Entity;
-import androidx.room.PrimaryKey;
-
-/** Table entity representing a key for an associated device. */
-@Entity(tableName = "associated_device_keys")
-public class AssociatedDeviceKeyEntity {
-
-    /** Id of the device. */
-    @PrimaryKey
-    @NonNull
-    public String id;
-
-    @NonNull
-    public String encryptedKey;
-
-    public AssociatedDeviceKeyEntity() { }
-
-    public AssociatedDeviceKeyEntity(String deviceId, String encryptedKey) {
-        id = deviceId;
-        this.encryptedKey = encryptedKey;
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceDatabase.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceDatabase.java
deleted file mode 100644
index 98a7b0a..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceDatabase.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.storage;
-
-import androidx.room.Database;
-import androidx.room.RoomDatabase;
-
-/** Database for connected devices. */
-@Database(entities = { AssociatedDeviceEntity.class, AssociatedDeviceKeyEntity.class,
-        AssociatedDeviceChallengeSecretEntity.class },
-        version = 2,
-        exportSchema = false)
-public abstract class ConnectedDeviceDatabase extends RoomDatabase {
-
-    /** Return the DAO for the associated device table. */
-    public abstract AssociatedDeviceDao associatedDeviceDao();
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorage.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorage.java
deleted file mode 100644
index b62a43c..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorage.java
+++ /dev/null
@@ -1,564 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.storage;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-import static com.android.car.connecteddevice.util.SafeLog.loge;
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.security.keystore.KeyGenParameterSpec;
-import android.security.keystore.KeyProperties;
-import android.util.Base64;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-import androidx.room.Room;
-
-import com.android.car.connecteddevice.R;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.InvalidParameterException;
-import java.security.Key;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.UnrecoverableKeyException;
-import java.security.cert.CertificateException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.Mac;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.GCMParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/** Storage for connected devices in a car. */
-public class ConnectedDeviceStorage {
-    private static final String TAG = "CompanionStorage";
-
-    private static final String UNIQUE_ID_KEY = "CTABM_unique_id";
-    private static final String BT_NAME_KEY = "CTABM_bt_name";
-    private static final String KEY_ALIAS = "Ukey2Key";
-    private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
-    private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
-    private static final String DATABASE_NAME = "connected-device-database";
-    private static final String IV_SPEC_SEPARATOR = ";";
-
-    private static final String CHALLENGE_HASHING_ALGORITHM = "HmacSHA256";
-    // This delimiter separates deviceId and deviceInfo, so it has to differ from the
-    // TrustedDeviceInfo delimiter. Once new API can be added, deviceId will be added to
-    // TrustedDeviceInfo and this delimiter will be removed.
-
-    // The length of the authentication tag for a cipher in GCM mode. The GCM specification states
-    // that this length can only have the values {128, 120, 112, 104, 96}. Using the highest
-    // possible value.
-    private static final int GCM_AUTHENTICATION_TAG_LENGTH = 128;
-
-    @VisibleForTesting
-    static final int CHALLENGE_SECRET_BYTES = 32;
-
-    private final Context mContext;
-
-    private SharedPreferences mSharedPreferences;
-
-    private UUID mUniqueId;
-
-    private AssociatedDeviceDao mAssociatedDeviceDatabase;
-
-    private AssociatedDeviceCallback mAssociatedDeviceCallback;
-
-    public ConnectedDeviceStorage(@NonNull Context context) {
-        mContext = context;
-        mAssociatedDeviceDatabase = Room.databaseBuilder(context, ConnectedDeviceDatabase.class,
-                DATABASE_NAME)
-                .fallbackToDestructiveMigration()
-                .build()
-                .associatedDeviceDao();
-    }
-
-    /**
-     * Set a callback for associated device updates.
-     *
-     * @param callback {@link AssociatedDeviceCallback} to set.
-     */
-    public void setAssociatedDeviceCallback(
-            @NonNull AssociatedDeviceCallback callback) {
-        mAssociatedDeviceCallback = callback;
-    }
-
-    /** Clear the callback for association device callback updates. */
-    public void clearAssociationDeviceCallback() {
-        mAssociatedDeviceCallback = null;
-    }
-
-    /**
-     * Get communication encryption key for the given device.
-     *
-     * @param deviceId id of trusted device
-     * @return encryption key, null if device id is not recognized
-     */
-    @Nullable
-    public byte[] getEncryptionKey(@NonNull String deviceId) {
-        AssociatedDeviceKeyEntity entity =
-                mAssociatedDeviceDatabase.getAssociatedDeviceKey(deviceId);
-        if (entity == null) {
-            logd(TAG, "Encryption key not found!");
-            return null;
-        }
-        String[] values = entity.encryptedKey.split(IV_SPEC_SEPARATOR, -1);
-
-        if (values.length != 2) {
-            logd(TAG, "Stored encryption key had the wrong length.");
-            return null;
-        }
-
-        byte[] encryptedKey = Base64.decode(values[0], Base64.DEFAULT);
-        byte[] ivSpec = Base64.decode(values[1], Base64.DEFAULT);
-        return decryptWithKeyStore(KEY_ALIAS, encryptedKey, ivSpec);
-    }
-
-    /**
-     * Save encryption key for the given device.
-     *
-     * @param deviceId id of the device
-     * @param encryptionKey encryption key
-     */
-    public void saveEncryptionKey(@NonNull String deviceId, @NonNull byte[] encryptionKey) {
-        String encryptedKey = encryptWithKeyStore(KEY_ALIAS, encryptionKey);
-        AssociatedDeviceKeyEntity entity = new AssociatedDeviceKeyEntity(deviceId, encryptedKey);
-        mAssociatedDeviceDatabase.addOrReplaceAssociatedDeviceKey(entity);
-        logd(TAG, "Successfully wrote encryption key.");
-    }
-
-    /**
-     * Save challenge secret for the given device.
-     *
-     * @param deviceId id of the device
-     * @param secret   Secret associated with this device. Note: must be
-     *                 {@value CHALLENGE_SECRET_BYTES} bytes in length or an
-     *                 {@link InvalidParameterException} will be thrown.
-     */
-    public void saveChallengeSecret(@NonNull String deviceId, @NonNull byte[] secret) {
-        if (secret.length != CHALLENGE_SECRET_BYTES) {
-            throw new InvalidParameterException("Secrets must be " + CHALLENGE_SECRET_BYTES
-                    + " bytes in length.");
-        }
-
-        String encryptedKey = encryptWithKeyStore(KEY_ALIAS, secret);
-        AssociatedDeviceChallengeSecretEntity entity = new AssociatedDeviceChallengeSecretEntity(
-                deviceId, encryptedKey);
-        mAssociatedDeviceDatabase.addOrReplaceAssociatedDeviceChallengeSecret(entity);
-        logd(TAG, "Successfully wrote challenge secret.");
-    }
-
-    /** Get the challenge secret associated with a device. */
-    public byte[] getChallengeSecret(@NonNull String deviceId) {
-        AssociatedDeviceChallengeSecretEntity entity =
-                mAssociatedDeviceDatabase.getAssociatedDeviceChallengeSecret(deviceId);
-        if (entity == null) {
-            logd(TAG, "Challenge secret not found!");
-            return null;
-        }
-        String[] values = entity.encryptedChallengeSecret.split(IV_SPEC_SEPARATOR, -1);
-
-        if (values.length != 2) {
-            logd(TAG, "Stored encryption key had the wrong length.");
-            return null;
-        }
-
-        byte[] encryptedSecret = Base64.decode(values[0], Base64.DEFAULT);
-        byte[] ivSpec = Base64.decode(values[1], Base64.DEFAULT);
-        return decryptWithKeyStore(KEY_ALIAS, encryptedSecret, ivSpec);
-    }
-
-    /**
-     * Hash provided value with device's challenge secret and return result. Returns {@code null} if
-     * unsuccessful.
-     */
-    @Nullable
-    public byte[] hashWithChallengeSecret(@NonNull String deviceId, @NonNull byte[] value) {
-        byte[] challengeSecret = getChallengeSecret(deviceId);
-        if (challengeSecret == null) {
-            loge(TAG, "Unable to find challenge secret for device " + deviceId + ".");
-            return null;
-        }
-
-        Mac mac;
-        try {
-            mac = Mac.getInstance(CHALLENGE_HASHING_ALGORITHM);
-        } catch (NoSuchAlgorithmException e) {
-            loge(TAG, "Unable to find hashing algorithm " + CHALLENGE_HASHING_ALGORITHM + ".", e);
-            return null;
-        }
-
-        SecretKeySpec keySpec = new SecretKeySpec(challengeSecret, CHALLENGE_HASHING_ALGORITHM);
-        try {
-            mac.init(keySpec);
-        } catch (InvalidKeyException e) {
-            loge(TAG, "Exception while initializing HMAC.", e);
-            return null;
-        }
-
-        return mac.doFinal(value);
-    }
-
-    /**
-     * Encrypt value with designated key
-     *
-     * <p>The encrypted value is of the form:
-     *
-     * <p>key + IV_SPEC_SEPARATOR + ivSpec
-     *
-     * <p>The {@code ivSpec} is needed to decrypt this key later on.
-     *
-     * @param keyAlias KeyStore alias for key to use
-     * @param value    a value to encrypt
-     * @return encrypted value, null if unable to encrypt
-     */
-    @Nullable
-    private String encryptWithKeyStore(@NonNull String keyAlias, @Nullable byte[] value) {
-        if (value == null) {
-            logw(TAG, "Received a null key value.");
-            return null;
-        }
-
-        Key key = getKeyStoreKey(keyAlias);
-        try {
-            Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
-            cipher.init(Cipher.ENCRYPT_MODE, key);
-            return Base64.encodeToString(cipher.doFinal(value), Base64.DEFAULT)
-                    + IV_SPEC_SEPARATOR
-                    + Base64.encodeToString(cipher.getIV(), Base64.DEFAULT);
-        } catch (IllegalBlockSizeException
-                | BadPaddingException
-                | NoSuchAlgorithmException
-                | NoSuchPaddingException
-                | IllegalStateException
-                | InvalidKeyException e) {
-            loge(TAG, "Unable to encrypt value with key " + keyAlias, e);
-            return null;
-        }
-    }
-
-    /**
-     * Decrypt value with designated key
-     *
-     * @param keyAlias KeyStore alias for key to use
-     * @param value    encrypted value
-     * @return decrypted value, null if unable to decrypt
-     */
-    @Nullable
-    private byte[] decryptWithKeyStore(
-            @NonNull String keyAlias, @Nullable byte[] value, @NonNull byte[] ivSpec) {
-        if (value == null) {
-            return null;
-        }
-
-        try {
-            Key key = getKeyStoreKey(keyAlias);
-            Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
-            cipher.init(
-                    Cipher.DECRYPT_MODE, key,
-                    new GCMParameterSpec(GCM_AUTHENTICATION_TAG_LENGTH, ivSpec));
-            return cipher.doFinal(value);
-        } catch (IllegalBlockSizeException
-                | BadPaddingException
-                | NoSuchAlgorithmException
-                | NoSuchPaddingException
-                | IllegalStateException
-                | InvalidKeyException
-                | InvalidAlgorithmParameterException e) {
-            loge(TAG, "Unable to decrypt value with key " + keyAlias, e);
-            return null;
-        }
-    }
-
-    @Nullable
-    private static Key getKeyStoreKey(@NonNull String keyAlias) {
-        KeyStore keyStore;
-        try {
-            keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
-            keyStore.load(null);
-            if (!keyStore.containsAlias(keyAlias)) {
-                KeyGenerator keyGenerator =
-                        KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
-                                KEYSTORE_PROVIDER);
-                keyGenerator.init(
-                        new KeyGenParameterSpec.Builder(
-                                keyAlias,
-                                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
-                                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
-                                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
-                                .build());
-                keyGenerator.generateKey();
-            }
-            return keyStore.getKey(keyAlias, null);
-
-        } catch (KeyStoreException
-                | NoSuchAlgorithmException
-                | UnrecoverableKeyException
-                | NoSuchProviderException
-                | CertificateException
-                | IOException
-                | InvalidAlgorithmParameterException e) {
-            loge(TAG, "Unable to retrieve key " + keyAlias + " from KeyStore.", e);
-            throw new IllegalStateException(e);
-        }
-    }
-
-    @NonNull
-    private SharedPreferences getSharedPrefs() {
-        // This should be called only after user 0 is unlocked.
-        if (mSharedPreferences != null) {
-            return mSharedPreferences;
-        }
-        mSharedPreferences = mContext.getSharedPreferences(
-                mContext.getString(R.string.connected_device_shared_preferences),
-                Context.MODE_PRIVATE);
-        return mSharedPreferences;
-
-    }
-
-    /**
-     * Get the unique id for head unit. Persists on device until factory reset. This should be
-     * called only after user 0 is unlocked.
-     *
-     * @return unique id
-     */
-    @NonNull
-    public UUID getUniqueId() {
-        if (mUniqueId != null) {
-            return mUniqueId;
-        }
-
-        SharedPreferences prefs = getSharedPrefs();
-        if (prefs.contains(UNIQUE_ID_KEY)) {
-            mUniqueId = UUID.fromString(prefs.getString(UNIQUE_ID_KEY, null));
-            logd(TAG,
-                    "Found existing trusted unique id: " + prefs.getString(UNIQUE_ID_KEY, ""));
-        }
-
-        if (mUniqueId == null) {
-            mUniqueId = UUID.randomUUID();
-            prefs.edit().putString(UNIQUE_ID_KEY, mUniqueId.toString()).apply();
-            logd(TAG,
-                    "Generated new trusted unique id: " + prefs.getString(UNIQUE_ID_KEY, ""));
-        }
-
-        return mUniqueId;
-    }
-
-    /** Store the current bluetooth adapter name. */
-    public void storeBluetoothName(@NonNull String name) {
-        getSharedPrefs().edit().putString(BT_NAME_KEY, name).apply();
-    }
-
-    /** Get the previously stored bluetooth adapter name or {@code null} if not found. */
-    @Nullable
-    public String getStoredBluetoothName() {
-        return getSharedPrefs().getString(BT_NAME_KEY, null);
-    }
-
-    /** Remove the previously stored bluetooth adapter name from storage. */
-    public void removeStoredBluetoothName() {
-        getSharedPrefs().edit().remove(BT_NAME_KEY).apply();
-    }
-
-    /**
-     * Get a list of associated devices for the given user.
-     *
-     * @param userId The identifier of the user.
-     * @return Associated device list.
-     */
-    @NonNull
-    public List<AssociatedDevice> getAssociatedDevicesForUser(@NonNull int userId) {
-        List<AssociatedDeviceEntity> entities =
-                mAssociatedDeviceDatabase.getAssociatedDevicesForUser(userId);
-
-        if (entities == null) {
-            return new ArrayList<>();
-        }
-
-        ArrayList<AssociatedDevice> userDevices = new ArrayList<>();
-        for (AssociatedDeviceEntity entity : entities) {
-            userDevices.add(entity.toAssociatedDevice());
-        }
-
-        return userDevices;
-    }
-
-    /**
-     * Get a list of associated devices for the current user.
-     *
-     * @return Associated device list.
-     */
-    @NonNull
-    public List<AssociatedDevice> getActiveUserAssociatedDevices() {
-        return getAssociatedDevicesForUser(ActivityManager.getCurrentUser());
-    }
-
-    /**
-     * Returns a list of device ids of associated devices for the given user.
-     *
-     * @param userId The user id for whom we want to know the device ids.
-     * @return List of device ids.
-     */
-    @NonNull
-    public List<String> getAssociatedDeviceIdsForUser(@NonNull int userId) {
-        List<AssociatedDevice> userDevices = getAssociatedDevicesForUser(userId);
-        ArrayList<String> userDeviceIds = new ArrayList<>();
-
-        for (AssociatedDevice device : userDevices) {
-            userDeviceIds.add(device.getDeviceId());
-        }
-
-        return userDeviceIds;
-    }
-
-    /**
-     * Returns a list of device ids of associated devices for the current user.
-     *
-     * @return List of device ids.
-     */
-    @NonNull
-    public List<String> getActiveUserAssociatedDeviceIds() {
-        return getAssociatedDeviceIdsForUser(ActivityManager.getCurrentUser());
-    }
-
-    /**
-     * Add the associated device of the given deviceId for the currently active user.
-     *
-     * @param device New associated device to be added.
-     */
-    public void addAssociatedDeviceForActiveUser(@NonNull AssociatedDevice device) {
-        addAssociatedDeviceForUser(ActivityManager.getCurrentUser(), device);
-        if (mAssociatedDeviceCallback != null) {
-            mAssociatedDeviceCallback.onAssociatedDeviceAdded(device);
-        }
-    }
-
-
-    /**
-     * Add the associated device of the given deviceId for the given user.
-     *
-     * @param userId The identifier of the user.
-     * @param device New associated device to be added.
-     */
-    public void addAssociatedDeviceForUser(int userId, @NonNull AssociatedDevice device) {
-        AssociatedDeviceEntity entity = new AssociatedDeviceEntity(userId, device,
-                /* isConnectionEnabled= */ true);
-        mAssociatedDeviceDatabase.addOrReplaceAssociatedDevice(entity);
-    }
-
-    /**
-     * Update the name for an associated device.
-     *
-     * @param deviceId The id of the associated device.
-     * @param name The name to replace with.
-     */
-    public void updateAssociatedDeviceName(@NonNull String deviceId, @NonNull String name) {
-        AssociatedDeviceEntity entity = mAssociatedDeviceDatabase.getAssociatedDevice(deviceId);
-        if (entity == null) {
-            logw(TAG, "Attempt to update name on an unrecognized device " + deviceId
-                    + ". Ignoring.");
-            return;
-        }
-        entity.name = name;
-        mAssociatedDeviceDatabase.addOrReplaceAssociatedDevice(entity);
-        if (mAssociatedDeviceCallback != null) {
-            mAssociatedDeviceCallback.onAssociatedDeviceUpdated(new AssociatedDevice(deviceId,
-                    entity.address, name, entity.isConnectionEnabled));
-        }
-    }
-
-    /**
-     * Remove the associated device of the given deviceId for the given user.
-     *
-     * @param userId The identifier of the user.
-     * @param deviceId The identifier of the device to be cleared.
-     */
-    public void removeAssociatedDevice(int userId, @NonNull String deviceId) {
-        AssociatedDeviceEntity entity = mAssociatedDeviceDatabase.getAssociatedDevice(deviceId);
-        if (entity == null || entity.userId != userId) {
-            return;
-        }
-        mAssociatedDeviceDatabase.removeAssociatedDevice(entity);
-        if (mAssociatedDeviceCallback != null) {
-            mAssociatedDeviceCallback.onAssociatedDeviceRemoved(new AssociatedDevice(deviceId,
-                    entity.address, entity.name, entity.isConnectionEnabled));
-        }
-    }
-
-    /**
-     * Clear the associated device of the given deviceId for the current user.
-     *
-     * @param deviceId The identifier of the device to be cleared.
-     */
-    public void removeAssociatedDeviceForActiveUser(@NonNull String deviceId) {
-        removeAssociatedDevice(ActivityManager.getCurrentUser(), deviceId);
-    }
-
-    /**
-     * Set if connection is enabled for an associated device.
-     *
-     * @param deviceId The id of the associated device.
-     * @param isConnectionEnabled If connection enabled for this device.
-     */
-    public void updateAssociatedDeviceConnectionEnabled(@NonNull String deviceId,
-            boolean isConnectionEnabled) {
-        AssociatedDeviceEntity entity = mAssociatedDeviceDatabase.getAssociatedDevice(deviceId);
-        if (entity == null) {
-            logw(TAG, "Attempt to enable or disable connection on an unrecognized device "
-                    + deviceId + ". Ignoring.");
-            return;
-        }
-        if (entity.isConnectionEnabled == isConnectionEnabled) {
-            return;
-        }
-        entity.isConnectionEnabled = isConnectionEnabled;
-        mAssociatedDeviceDatabase.addOrReplaceAssociatedDevice(entity);
-        if (mAssociatedDeviceCallback != null) {
-            mAssociatedDeviceCallback.onAssociatedDeviceUpdated(new AssociatedDevice(deviceId,
-                    entity.address, entity.name, isConnectionEnabled));
-        }
-    }
-
-    /** Callback for association device related events. */
-    public interface AssociatedDeviceCallback {
-        /** Triggered when an associated device has been added. */
-        void onAssociatedDeviceAdded(@NonNull AssociatedDevice device);
-
-        /** Triggered when an associated device has been removed. */
-        void onAssociatedDeviceRemoved(@NonNull AssociatedDevice device);
-
-        /** Triggered when an associated device has been updated. */
-        void onAssociatedDeviceUpdated(@NonNull AssociatedDevice device);
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/ByteUtils.java b/connected-device-lib/src/com/android/car/connecteddevice/util/ByteUtils.java
deleted file mode 100644
index adffa73..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/ByteUtils.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.util;
-
-import android.annotation.SuppressLint;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.UUID;
-import java.util.concurrent.ThreadLocalRandom;
-
-/**
- * Utility classes for manipulating bytes.
- */
-public final class ByteUtils {
-    // https://developer.android.com/reference/java/util/UUID
-    private static final int UUID_LENGTH = 16;
-
-    private ByteUtils() {
-    }
-
-    /**
-     * Returns a byte buffer corresponding to the passed long argument.
-     *
-     * @param primitive data to convert format.
-     */
-    public static byte[] longToBytes(long primitive) {
-        ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
-        buffer.putLong(primitive);
-        return buffer.array();
-    }
-
-    /**
-     * Returns a byte buffer corresponding to the passed long argument.
-     *
-     * @param array data to convert format.
-     */
-    public static long bytesToLong(byte[] array) {
-        ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
-        buffer.put(array);
-        buffer.flip();
-        long value = buffer.getLong();
-        return value;
-    }
-
-    /**
-     * Returns a String in Hex format that is formed from the bytes in the byte array Useful for
-     * debugging
-     *
-     * @param array the byte array
-     * @return the Hex string version of the input byte array
-     */
-    public static String byteArrayToHexString(byte[] array) {
-        StringBuilder sb = new StringBuilder(array.length * 2);
-        for (byte b : array) {
-            sb.append(String.format("%02x", b));
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Convert UUID to Big Endian byte array
-     *
-     * @param uuid UUID to convert
-     * @return the byte array representing the UUID
-     */
-    @NonNull
-    public static byte[] uuidToBytes(@NonNull UUID uuid) {
-
-        return ByteBuffer.allocate(UUID_LENGTH)
-                .order(ByteOrder.BIG_ENDIAN)
-                .putLong(uuid.getMostSignificantBits())
-                .putLong(uuid.getLeastSignificantBits())
-                .array();
-    }
-
-    /**
-     * Convert Big Endian byte array to UUID
-     *
-     * @param bytes byte array to convert
-     * @return the UUID representing the byte array, or null if not a valid UUID
-     */
-    @Nullable
-    public static UUID bytesToUUID(@NonNull byte[] bytes) {
-        if (bytes.length != UUID_LENGTH) {
-            return null;
-        }
-
-        ByteBuffer buffer = ByteBuffer.wrap(bytes);
-        return new UUID(buffer.getLong(), buffer.getLong());
-    }
-
-    /**
-     * Generate a random zero-filled string of given length
-     *
-     * @param length of string
-     * @return generated string
-     */
-    @SuppressLint("DefaultLocale") // Should always have the same format regardless of locale
-    public static String generateRandomNumberString(int length) {
-        return String.format(
-                "%0" + length + "d",
-                ThreadLocalRandom.current().nextInt((int) Math.pow(10, length)));
-    }
-
-    /**
-     * Generate a {@link byte[]} with random bytes.
-     *
-     * @param size of array to generate.
-     * @return generated {@link byte[]}.
-     */
-    @NonNull
-    public static byte[] randomBytes(int size) {
-        byte[] array = new byte[size];
-        ThreadLocalRandom.current().nextBytes(array);
-        return array;
-    }
-
-    /**
-     * Concatentate the given 2 byte arrays
-     *
-     * @param a input array 1
-     * @param b input array 2
-     * @return concatenated array of arrays 1 and 2
-     */
-    @Nullable
-    public static byte[] concatByteArrays(@Nullable byte[] a, @Nullable byte[] b) {
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        try {
-            if (a != null) {
-                outputStream.write(a);
-            }
-            if (b != null) {
-                outputStream.write(b);
-            }
-        } catch (IOException e) {
-            return null;
-        }
-        return outputStream.toByteArray();
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/EventLog.java b/connected-device-lib/src/com/android/car/connecteddevice/util/EventLog.java
deleted file mode 100644
index 5dcd829..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/EventLog.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.util;
-
-import static com.android.car.connecteddevice.util.SafeLog.logi;
-
-import com.android.car.connecteddevice.ConnectedDeviceManager;
-
-/** Logging class for collecting metrics. */
-public class EventLog {
-
-    private static final String TAG = "ConnectedDeviceEvent";
-
-    private EventLog() { }
-
-    /** Mark in log that the service has started. */
-    public static void onServiceStarted() {
-        logi(TAG, "SERVICE_STARTED");
-    }
-
-    /** Mark in log that the {@link ConnectedDeviceManager} has started. */
-    public static void onConnectedDeviceManagerStarted() {
-        logi(TAG, "CONNECTED_DEVICE_MANAGER_STARTED");
-    }
-
-    /** Mark in the log that BLE is on. */
-    public static void onBleOn() {
-        logi(TAG, "BLE_ON");
-    }
-
-    /** Mark in the log that a search for the user's device has started. */
-    public static void onStartDeviceSearchStarted() {
-        logi(TAG, "SEARCHING_FOR_DEVICE");
-    }
-
-
-    /** Mark in the log that a device connected. */
-    public static void onDeviceConnected() {
-        logi(TAG, "DEVICE_CONNECTED");
-    }
-
-    /** Mark in the log that the device has sent its id. */
-    public static void onDeviceIdReceived() {
-        logi(TAG, "RECEIVED_DEVICE_ID");
-    }
-
-    /** Mark in the log that a secure channel has been established with a device. */
-    public static void onSecureChannelEstablished() {
-        logi(TAG, "SECURE_CHANNEL_ESTABLISHED");
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/RemoteCallbackBinder.java b/connected-device-lib/src/com/android/car/connecteddevice/util/RemoteCallbackBinder.java
deleted file mode 100644
index 98ad4db..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/RemoteCallbackBinder.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.util;
-
-import static com.android.car.connecteddevice.util.SafeLog.logd;
-
-import android.os.IBinder;
-import android.os.RemoteException;
-
-import java.util.function.Consumer;
-
-/**
- * Class that holds the binder of a remote callback and an action to be executed when this
- * binder dies.
- * It registers for death notification of the {@link #mCallbackBinder} and executes
- * {@link #mOnDiedConsumer} when {@link #mCallbackBinder} dies.
- */
-public class RemoteCallbackBinder implements IBinder.DeathRecipient {
-    private static final String TAG = "BinderClient";
-    private final IBinder mCallbackBinder;
-    private final Consumer<IBinder> mOnDiedConsumer;
-
-    public RemoteCallbackBinder(IBinder binder, Consumer<IBinder> onBinderDied) {
-        mCallbackBinder = binder;
-        mOnDiedConsumer = onBinderDied;
-        try {
-            binder.linkToDeath(this, 0);
-        } catch (RemoteException e) {
-            logd(TAG, "Cannot link death recipient to binder " + mCallbackBinder + ", "
-                    + e);
-        }
-    }
-
-    @Override
-    public void binderDied() {
-        logd(TAG, "Binder died " + mCallbackBinder);
-        mOnDiedConsumer.accept(mCallbackBinder);
-        cleanUp();
-    }
-
-    /** Clean up the client. */
-    public void cleanUp() {
-        mCallbackBinder.unlinkToDeath(this, 0);
-    }
-
-    /** Get the callback binder of the client. */
-    public IBinder getCallbackBinder() {
-        return mCallbackBinder;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (obj == this) {
-            return true;
-        }
-        if (!(obj instanceof RemoteCallbackBinder)) {
-            return false;
-        }
-        RemoteCallbackBinder remoteCallbackBinder = (RemoteCallbackBinder) obj;
-        return mCallbackBinder.equals(remoteCallbackBinder.mCallbackBinder);
-    }
-
-    @Override
-    public int hashCode() {
-        return mCallbackBinder.hashCode();
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/SafeLog.java b/connected-device-lib/src/com/android/car/connecteddevice/util/SafeLog.java
deleted file mode 100644
index 8419e0f..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/SafeLog.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.util;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * Convenience logging methods that respect whitelisted tags.
- */
-public class SafeLog {
-
-    private SafeLog() { }
-
-    /** Log message if tag is whitelisted for {@code Log.VERBOSE}. */
-    public static void logv(@NonNull String tag, @NonNull String message) {
-        if (Log.isLoggable(tag, Log.VERBOSE)) {
-            Log.v(tag, message);
-        }
-    }
-
-    /** Log message if tag is whitelisted for {@code Log.INFO}. */
-    public static void logi(@NonNull String tag, @NonNull String message) {
-        if (Log.isLoggable(tag, Log.INFO)) {
-            Log.i(tag, message);
-        }
-    }
-
-    /** Log message if tag is whitelisted for {@code Log.DEBUG}. */
-    public static void logd(@NonNull String tag, @NonNull String message) {
-        if (Log.isLoggable(tag, Log.DEBUG)) {
-            Log.d(tag, message);
-        }
-    }
-
-    /** Log message if tag is whitelisted for {@code Log.WARN}. */
-    public static void logw(@NonNull String tag, @NonNull String message) {
-        if (Log.isLoggable(tag, Log.WARN)) {
-            Log.w(tag, message);
-        }
-    }
-
-    /** Log message if tag is whitelisted for {@code Log.ERROR}. */
-    public static void loge(@NonNull String tag, @NonNull String message) {
-        loge(tag, message, /* exception= */ null);
-    }
-
-    /** Log message and optional exception if tag is whitelisted for {@code Log.ERROR}. */
-    public static void loge(@NonNull String tag, @NonNull String message,
-            @Nullable Exception exception) {
-        if (Log.isLoggable(tag, Log.ERROR)) {
-            Log.e(tag, message, exception);
-        }
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/ScanDataAnalyzer.java b/connected-device-lib/src/com/android/car/connecteddevice/util/ScanDataAnalyzer.java
deleted file mode 100644
index fcd6dc5..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/ScanDataAnalyzer.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.util;
-
-import static com.android.car.connecteddevice.util.SafeLog.logw;
-
-import android.bluetooth.le.ScanResult;
-
-import androidx.annotation.NonNull;
-
-import java.math.BigInteger;
-
-/**
- * Analyzer of {@link ScanResult} data to identify an Apple device that is advertising from the
- * background.
- */
-public class ScanDataAnalyzer {
-
-    private static final String TAG = "ScanDataAnalyzer";
-
-    private static final byte IOS_OVERFLOW_LENGTH = (byte) 0x14;
-    private static final byte IOS_ADVERTISING_TYPE = (byte) 0xff;
-    private static final int IOS_ADVERTISING_TYPE_LENGTH = 1;
-    private static final long IOS_OVERFLOW_CUSTOM_ID = 0x4c0001;
-    private static final int IOS_OVERFLOW_CUSTOM_ID_LENGTH = 3;
-    private static final int IOS_OVERFLOW_CONTENT_LENGTH =
-            IOS_OVERFLOW_LENGTH - IOS_OVERFLOW_CUSTOM_ID_LENGTH - IOS_ADVERTISING_TYPE_LENGTH;
-
-    private ScanDataAnalyzer() { }
-
-    /**
-     * Returns {@code true} if the given bytes from a [ScanResult] contains service UUIDs once the
-     * given serviceUuidMask is applied.
-     *
-     * When an iOS peripheral device goes into a background state, the service UUIDs and other
-     * identifying information are removed from the advertising data and replaced with a hashed
-     * bit in a special "overflow" area. There is no documentation on the layout of this area,
-     * and the below was compiled from experimentation and examples from others who have worked
-     * on reverse engineering iOS background peripherals.
-     *
-     * My best guess is Apple is taking the service UUID and hashing it into a bloom filter. This
-     * would allow any device with the same hashing function to filter for all devices that
-     * might contain the desired service. Since we do not have access to this hashing function,
-     * we must first advertise our service from an iOS device and manually inspect the bit that
-     * is flipped. Once known, it can be passed to serviceUuidMask and used as a filter.
-     *
-     * EXAMPLE
-     *
-     * Foreground contents:
-     * 02011A1107FB349B5F8000008000100000C53A00000709546573746572000000000000000000000000000000000000000000000000000000000000000000
-     *
-     * Background contents:
-     * 02011A14FF4C0001000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000
-     *
-     * The overflow bytes are comprised of four parts:
-     * Length -> 14
-     * Advertising type -> FF
-     * Id custom to Apple -> 4C0001
-     * Contents where hashed values are stored -> 00000000000000000000000000200000
-     *
-     * Apple's documentation on advertising from the background:
-     * https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html#//apple_ref/doc/uid/TP40013257-CH7-SW9
-     *
-     * Other similar reverse engineering:
-     * http://www.pagepinner.com/2014/04/how-to-get-ble-overflow-hash-bit-from.html
-     */
-    public static boolean containsUuidsInOverflow(@NonNull byte[] scanData,
-            @NonNull BigInteger serviceUuidMask) {
-        byte[] overflowBytes = new byte[IOS_OVERFLOW_CONTENT_LENGTH];
-        int overflowPtr = 0;
-        int outPtr = 0;
-        try {
-            while (overflowPtr < scanData.length - IOS_OVERFLOW_LENGTH) {
-                byte length = scanData[overflowPtr++];
-                if (length == 0) {
-                    break;
-                } else if (length != IOS_OVERFLOW_LENGTH) {
-                    continue;
-                }
-
-                if (scanData[overflowPtr++] != IOS_ADVERTISING_TYPE) {
-                    return false;
-                }
-
-                byte[] idBytes = new byte[IOS_OVERFLOW_CUSTOM_ID_LENGTH];
-                for (int i = 0; i < IOS_OVERFLOW_CUSTOM_ID_LENGTH; i++) {
-                    idBytes[i] = scanData[overflowPtr++];
-                }
-
-                if (!new BigInteger(idBytes).equals(BigInteger.valueOf(IOS_OVERFLOW_CUSTOM_ID))) {
-                    return false;
-                }
-
-                for (outPtr = 0; outPtr < IOS_OVERFLOW_CONTENT_LENGTH; outPtr++) {
-                    overflowBytes[outPtr] = scanData[overflowPtr++];
-                }
-                break;
-            }
-
-            if (outPtr == IOS_OVERFLOW_CONTENT_LENGTH) {
-                BigInteger overflowBytesValue = new BigInteger(overflowBytes);
-                return overflowBytesValue.and(serviceUuidMask).signum() == 1;
-            }
-
-        } catch (ArrayIndexOutOfBoundsException e) {
-            logw(TAG, "Inspecting advertisement overflow bytes went out of bounds.");
-        }
-
-        return false;
-    }
-}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/util/ThreadSafeCallbacks.java b/connected-device-lib/src/com/android/car/connecteddevice/util/ThreadSafeCallbacks.java
deleted file mode 100644
index 152c464..0000000
--- a/connected-device-lib/src/com/android/car/connecteddevice/util/ThreadSafeCallbacks.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.util;
-
-import androidx.annotation.NonNull;
-
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executor;
-import java.util.function.Consumer;
-
-/**
- * Class for invoking thread-safe callbacks.
- *
- * @param <T> Callback type.
- */
-public class ThreadSafeCallbacks<T> {
-
-    private final ConcurrentHashMap<T, Executor> mCallbacks = new ConcurrentHashMap<>();
-
-    /** Add a callback to be notified on its executor. */
-    public void add(@NonNull T callback, @NonNull Executor executor) {
-        mCallbacks.put(callback, executor);
-    }
-
-    /** Remove a callback from the collection. */
-    public void remove(@NonNull T callback) {
-        mCallbacks.remove(callback);
-    }
-
-    /** Clear all callbacks from the collection. */
-    public void clear() {
-        mCallbacks.clear();
-    }
-
-    /** Return the number of callbacks in collection. */
-    public int size() {
-        return mCallbacks.size();
-    }
-
-    /** Invoke notification on all callbacks with their supplied {@link Executor}. */
-    public void invoke(Consumer<T> notification) {
-        mCallbacks.forEach((callback, executor) ->
-                executor.execute(() -> notification.accept(callback)));
-    }
-}
diff --git a/connected-device-lib/tests/unit/Android.bp b/connected-device-lib/tests/unit/Android.bp
deleted file mode 100644
index 92810a4..0000000
--- a/connected-device-lib/tests/unit/Android.bp
+++ /dev/null
@@ -1,54 +0,0 @@
-//
-// Copyright (C) 2019 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.
-//
-
-android_test {
-    name: "connected-device-lib-unit-tests",
-
-    srcs: ["src/**/*.java"],
-
-    libs: [
-        "android.test.runner",
-        "android.test.base",
-        "android.test.mock",
-    ],
-
-    static_libs: [
-        "android.car",
-        "androidx.test.core",
-        "androidx.test.ext.junit",
-        "androidx.test.rules",
-        "connected-device-lib",
-        "mockito-target-extended-minus-junit4",
-        "testables",
-        // TODO: remove once Android migrates to JUnit 4.13,
-        // which provides assertThrows
-        "testng",
-        "truth-prebuilt",
-    ],
-
-    jni_libs: [
-        // For mockito extended
-        "libdexmakerjvmtiagent",
-        "libstaticjvmtiagent",
-    ],
-
-    sdk_version: "system_current",
-    min_sdk_version: "28",
-
-    certificate: "platform",
-
-    privileged: true,
-}
\ No newline at end of file
diff --git a/connected-device-lib/tests/unit/AndroidManifest.xml b/connected-device-lib/tests/unit/AndroidManifest.xml
deleted file mode 100644
index 9863ccf..0000000
--- a/connected-device-lib/tests/unit/AndroidManifest.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<!--
-  ~ Copyright (C) 2019 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.
-  -->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.car.connecteddevice.tests.unit">
-
-    <!--  Needed for BLE scanning/advertising -->
-    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-    <uses-permission android:name="android.permission.BLUETOOTH"/>
-    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
-
-    <!--  Needed for detecting foreground user -->
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
-    <uses-permission android:name="android.permission.MANAGE_USERS" />
-
-    <application android:testOnly="true"
-                 android:debuggable="true">
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.car.connecteddevice.tests.unit"
-                     android:label="Connected Device Lib Test Cases" />
-</manifest>
diff --git a/connected-device-lib/tests/unit/README.md b/connected-device-lib/tests/unit/README.md
deleted file mode 100644
index 4543058..0000000
--- a/connected-device-lib/tests/unit/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Instructions for running unit tests
-
-### Build unit test module
-
-`m connected-device-lib-unit-tests`
-
-### Install resulting apk on device
-
-`adb install -r -t $OUT/testcases/connected-device-lib-unit-tests/arm64/connected-device-lib-unit-tests.apk`
-
-### Run all tests
-
-`adb shell am instrument -w com.android.car.connecteddevice.tests.unit`
-
-### Run tests in a class
-
-`adb shell am instrument -w -e class com.android.car.connecteddevice.<classPath> com.android.car.connecteddevice.tests.unit`
-
-### Run a specific test
-
-`adb shell am instrument -w -e class com.android.car.connecteddevice.<classPath>#<testMethod> com.android.car.connecteddevice.tests.unit`
-
-More general information can be found at
-http://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html
\ No newline at end of file
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java
deleted file mode 100644
index fe0386c..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java
+++ /dev/null
@@ -1,743 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice;
-
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED;
-import static com.android.car.connecteddevice.ConnectedDeviceManager.DEVICE_ERROR_INVALID_SECURITY_KEY;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.ConnectedDeviceManager.ConnectionCallback;
-import com.android.car.connecteddevice.ConnectedDeviceManager.DeviceAssociationCallback;
-import com.android.car.connecteddevice.ConnectedDeviceManager.DeviceCallback;
-import com.android.car.connecteddevice.ConnectedDeviceManager.MessageDeliveryDelegate;
-import com.android.car.connecteddevice.connection.CarBluetoothManager;
-import com.android.car.connecteddevice.connection.DeviceMessage;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.model.ConnectedDevice;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage.AssociatedDeviceCallback;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class ConnectedDeviceManagerTest {
-
-    private static final String TEST_DEVICE_ADDRESS = "00:11:22:33:44:55";
-
-    private static final String TEST_DEVICE_NAME = "TEST_DEVICE_NAME";
-
-
-    private final Executor mCallbackExecutor = Executors.newSingleThreadExecutor();
-
-    private final UUID mRecipientId = UUID.randomUUID();
-
-    private final List<String> mUserDeviceIds = new ArrayList<>();
-
-    private final List<AssociatedDevice> mUserDevices = new ArrayList<>();
-
-    @Mock
-    private ConnectedDeviceStorage mMockStorage;
-
-    @Mock
-    private CarBluetoothManager mMockCarBluetoothManager;
-
-    private ConnectedDeviceManager mConnectedDeviceManager;
-
-    private MockitoSession mMockingSession;
-
-    private AssociatedDeviceCallback mAssociatedDeviceCallback;
-
-    @Before
-    public void setUp() {
-        mMockingSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-        ArgumentCaptor<AssociatedDeviceCallback> callbackCaptor = ArgumentCaptor
-                .forClass(AssociatedDeviceCallback.class);
-        mConnectedDeviceManager = new ConnectedDeviceManager(mMockCarBluetoothManager,
-                mMockStorage);
-        verify(mMockStorage).setAssociatedDeviceCallback(callbackCaptor.capture());
-        when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(mUserDevices);
-        when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(mUserDeviceIds);
-        mAssociatedDeviceCallback = callbackCaptor.getValue();
-        mConnectedDeviceManager.start();
-    }
-
-    @After
-    public void tearDown() {
-        if (mMockingSession != null) {
-            mMockingSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void getActiveUserConnectedDevices_initiallyShouldReturnEmptyList() {
-        assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
-    }
-
-    @Test
-    public void getActiveUserConnectedDevices_includesNewlyConnectedDevice() {
-        String deviceId = connectNewDevice();
-        List<ConnectedDevice> activeUserDevices =
-                mConnectedDeviceManager.getActiveUserConnectedDevices();
-        ConnectedDevice expectedDevice = new ConnectedDevice(deviceId, /* deviceName= */ null,
-                /* belongsToActiveUser= */ true, /* hasSecureChannel= */ false);
-        assertThat(activeUserDevices).containsExactly(expectedDevice);
-    }
-
-    @Test
-    public void getActiveUserConnectedDevices_excludesDevicesNotBelongingToActiveUser() {
-        String deviceId = UUID.randomUUID().toString();
-        String otherUserDeviceId = UUID.randomUUID().toString();
-        when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
-                Collections.singletonList(otherUserDeviceId));
-        mConnectedDeviceManager.addConnectedDevice(deviceId);
-        assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
-    }
-
-    @Test
-    public void getActiveUserConnectedDevices_reflectsSecureChannelEstablished() {
-        String deviceId = connectNewDevice();
-        mConnectedDeviceManager.onSecureChannelEstablished(deviceId);
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        assertThat(connectedDevice.hasSecureChannel()).isTrue();
-    }
-
-    @Test
-    public void getActiveUserConnectedDevices_excludesDisconnectedDevice() {
-        String deviceId = connectNewDevice();
-        mConnectedDeviceManager.removeConnectedDevice(deviceId);
-        assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
-    }
-
-    @Test(expected = IllegalStateException.class)
-    public void sendMessageSecurely_throwsIllegalStateExceptionIfNoSecureChannel() {
-        connectNewDevice();
-        ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        UUID recipientId = UUID.randomUUID();
-        byte[] message = ByteUtils.randomBytes(10);
-        mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
-    }
-
-    @Test
-    public void sendMessageSecurely_sendsEncryptedMessage() {
-        String deviceId = connectNewDevice();
-        mConnectedDeviceManager.onSecureChannelEstablished(deviceId);
-        ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        UUID recipientId = UUID.randomUUID();
-        byte[] message = ByteUtils.randomBytes(10);
-        mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
-        ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-        verify(mMockCarBluetoothManager).sendMessage(eq(deviceId), messageCaptor.capture());
-        assertThat(messageCaptor.getValue().isMessageEncrypted()).isTrue();
-    }
-
-    @Test
-    public void sendMessageSecurely_doesNotSendIfDeviceDisconnected() {
-        String deviceId = connectNewDevice();
-        ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        mConnectedDeviceManager.removeConnectedDevice(deviceId);
-        UUID recipientId = UUID.randomUUID();
-        byte[] message = ByteUtils.randomBytes(10);
-        mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
-        verify(mMockCarBluetoothManager, times(0)).sendMessage(eq(deviceId),
-                any(DeviceMessage.class));
-    }
-
-    @Test
-    public void sendMessageUnsecurely_sendsMessageWithoutEncryption() {
-        String deviceId = connectNewDevice();
-        ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        UUID recipientId = UUID.randomUUID();
-        byte[] message = ByteUtils.randomBytes(10);
-        mConnectedDeviceManager.sendMessageUnsecurely(device, recipientId, message);
-        ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-        verify(mMockCarBluetoothManager).sendMessage(eq(deviceId), messageCaptor.capture());
-        assertThat(messageCaptor.getValue().isMessageEncrypted()).isFalse();
-    }
-
-    @Test
-    public void connectionCallback_onDeviceConnectedInvokedForNewlyConnectedDevice()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
-        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
-                mCallbackExecutor);
-        String deviceId = connectNewDevice();
-        assertThat(tryAcquire(semaphore)).isTrue();
-        ArgumentCaptor<ConnectedDevice> deviceCaptor =
-                ArgumentCaptor.forClass(ConnectedDevice.class);
-        verify(connectionCallback).onDeviceConnected(deviceCaptor.capture());
-        ConnectedDevice connectedDevice = deviceCaptor.getValue();
-        assertThat(connectedDevice.getDeviceId()).isEqualTo(deviceId);
-        assertThat(connectedDevice.hasSecureChannel()).isFalse();
-    }
-
-    @Test
-    public void connectionCallback_onDeviceConnectedNotInvokedDeviceConnectedForDifferentUser()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
-        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
-                mCallbackExecutor);
-        String deviceId = UUID.randomUUID().toString();
-        String otherUserDeviceId = UUID.randomUUID().toString();
-        when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
-                Collections.singletonList(otherUserDeviceId));
-        mConnectedDeviceManager.addConnectedDevice(deviceId);
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void connectionCallback_onDeviceConnectedNotInvokedForDifferentBleManager()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        String deviceId = connectNewDevice();
-        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
-        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
-                mCallbackExecutor);
-        mConnectedDeviceManager.addConnectedDevice(deviceId);
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void connectionCallback_onDeviceDisconnectedInvokedForActiveUserDevice()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        String deviceId = connectNewDevice();
-        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
-        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
-                mCallbackExecutor);
-        mConnectedDeviceManager.removeConnectedDevice(deviceId);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        ArgumentCaptor<ConnectedDevice> deviceCaptor =
-                ArgumentCaptor.forClass(ConnectedDevice.class);
-        verify(connectionCallback).onDeviceDisconnected(deviceCaptor.capture());
-        assertThat(deviceCaptor.getValue().getDeviceId()).isEqualTo(deviceId);
-    }
-
-    @Test
-    public void connectionCallback_onDeviceDisconnectedNotInvokedDeviceForDifferentUser()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        String deviceId = UUID.randomUUID().toString();
-        mConnectedDeviceManager.addConnectedDevice(deviceId);
-        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
-        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
-                mCallbackExecutor);
-        mConnectedDeviceManager.removeConnectedDevice(deviceId);
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void unregisterConnectionCallback_removesCallbackAndNotInvoked()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
-        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
-                mCallbackExecutor);
-        mConnectedDeviceManager.unregisterConnectionCallback(connectionCallback);
-        connectNewDevice();
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void registerDeviceCallback_blacklistsDuplicateRecipientId()
-            throws InterruptedException {
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        Semaphore firstSemaphore = new Semaphore(0);
-        Semaphore secondSemaphore = new Semaphore(0);
-        Semaphore thirdSemaphore = new Semaphore(0);
-        DeviceCallback firstDeviceCallback = createDeviceCallback(firstSemaphore);
-        DeviceCallback secondDeviceCallback = createDeviceCallback(secondSemaphore);
-        DeviceCallback thirdDeviceCallback = createDeviceCallback(thirdSemaphore);
-
-        // Register three times for following chain of events:
-        // 1. First callback registered without issue.
-        // 2. Second callback with same recipientId triggers blacklisting both callbacks and issues
-        //    error callbacks on both. Both callbacks should be unregistered at this point.
-        // 3. Third callback gets rejected at registration and issues error callback.
-
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                firstDeviceCallback, mCallbackExecutor);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                secondDeviceCallback, mCallbackExecutor);
-        DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-        assertThat(tryAcquire(firstSemaphore)).isTrue();
-        assertThat(tryAcquire(secondSemaphore)).isTrue();
-        verify(firstDeviceCallback)
-                .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
-        verify(secondDeviceCallback)
-                .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
-        verify(firstDeviceCallback, times(0)).onMessageReceived(any(), any());
-        verify(secondDeviceCallback, times(0)).onMessageReceived(any(), any());
-
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                thirdDeviceCallback, mCallbackExecutor);
-        assertThat(tryAcquire(thirdSemaphore)).isTrue();
-        verify(thirdDeviceCallback)
-                .onDeviceError(connectedDevice, DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED);
-    }
-
-    @Test
-    public void deviceCallback_onSecureChannelEstablishedInvoked() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId());
-        connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(deviceCallback).onSecureChannelEstablished(connectedDevice);
-    }
-
-    @Test
-    public void deviceCallback_onMessageReceivedInvokedForSameRecipientId()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        byte[] payload = ByteUtils.randomBytes(10);
-        DeviceMessage message = new DeviceMessage(mRecipientId, false, payload);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(deviceCallback).onMessageReceived(connectedDevice, payload);
-    }
-
-    @Test
-    public void deviceCallback_onMessageReceivedNotInvokedForDifferentRecipientId()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        byte[] payload = ByteUtils.randomBytes(10);
-        DeviceMessage message = new DeviceMessage(UUID.randomUUID(), false, payload);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void deviceCallback_onDeviceErrorInvokedOnChannelError() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        mConnectedDeviceManager.deviceErrorOccurred(connectedDevice.getDeviceId());
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(deviceCallback).onDeviceError(connectedDevice, DEVICE_ERROR_INVALID_SECURITY_KEY);
-    }
-
-    @Test
-    public void unregisterDeviceCallback_removesCallbackAndNotInvoked()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        mConnectedDeviceManager.unregisterDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback);
-        mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId());
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void registerDeviceCallback_sendsMissedMessageAfterRegistration()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        byte[] payload = ByteUtils.randomBytes(10);
-        DeviceMessage message = new DeviceMessage(mRecipientId, false, payload);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(deviceCallback).onMessageReceived(connectedDevice, payload);
-    }
-
-    @Test
-    public void registerDeviceCallback_sendsMultipleMissedMessagesAfterRegistration()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        byte[] payload1 = ByteUtils.randomBytes(10);
-        byte[] payload2 = ByteUtils.randomBytes(10);
-        DeviceMessage message1 = new DeviceMessage(mRecipientId, false, payload1);
-        DeviceMessage message2 = new DeviceMessage(mRecipientId, false, payload2);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message1);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message2);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(deviceCallback).onMessageReceived(connectedDevice, payload1);
-        verify(deviceCallback, timeout(1000)).onMessageReceived(connectedDevice, payload2);
-    }
-
-    @Test
-    public void registerDeviceCallback_doesNotSendMissedMessageForDifferentRecipient()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        byte[] payload = ByteUtils.randomBytes(10);
-        DeviceMessage message = new DeviceMessage(UUID.randomUUID(), false, payload);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void registerDeviceCallback_doesNotSendMissedMessageForDifferentDevice()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        connectNewDevice();
-        connectNewDevice();
-        List<ConnectedDevice> connectedDevices =
-                mConnectedDeviceManager.getActiveUserConnectedDevices();
-        ConnectedDevice connectedDevice = connectedDevices.get(0);
-        ConnectedDevice otherDevice = connectedDevices.get(1);
-        byte[] payload = ByteUtils.randomBytes(10);
-        DeviceMessage message = new DeviceMessage(mRecipientId, false, payload);
-        mConnectedDeviceManager.onMessageReceived(otherDevice.getDeviceId(), message);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void onAssociationCompleted_disconnectsOriginalDeviceAndReconnectsAsActiveUser()
-            throws InterruptedException {
-        String deviceId = UUID.randomUUID().toString();
-        mConnectedDeviceManager.addConnectedDevice(deviceId);
-        Semaphore semaphore = new Semaphore(0);
-        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
-        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
-                mCallbackExecutor);
-        when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
-                Collections.singletonList(deviceId));
-        mConnectedDeviceManager.onAssociationCompleted(deviceId);
-        assertThat(tryAcquire(semaphore)).isTrue();
-    }
-
-    private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
-        return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
-    }
-
-    @Test
-    public void deviceAssociationCallback_onAssociatedDeviceAdded() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        DeviceAssociationCallback callback = createDeviceAssociationCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceAssociationCallback(callback, mCallbackExecutor);
-        String deviceId = UUID.randomUUID().toString();
-        AssociatedDevice testDevice = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
-                TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-        mAssociatedDeviceCallback.onAssociatedDeviceAdded(testDevice);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(callback).onAssociatedDeviceAdded(eq(testDevice));
-    }
-
-    @Test
-    public void deviceAssociationCallback_onAssociationDeviceRemoved() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        DeviceAssociationCallback callback = createDeviceAssociationCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceAssociationCallback(callback, mCallbackExecutor);
-        String deviceId = UUID.randomUUID().toString();
-        AssociatedDevice testDevice = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
-                TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-        mAssociatedDeviceCallback.onAssociatedDeviceRemoved(testDevice);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(callback).onAssociatedDeviceRemoved(eq(testDevice));
-    }
-
-    @Test
-    public void deviceAssociationCallback_onAssociatedDeviceUpdated() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        DeviceAssociationCallback callback = createDeviceAssociationCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceAssociationCallback(callback, mCallbackExecutor);
-        String deviceId = UUID.randomUUID().toString();
-        AssociatedDevice testDevice = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
-                TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-        mAssociatedDeviceCallback.onAssociatedDeviceUpdated(testDevice);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(callback).onAssociatedDeviceUpdated(eq(testDevice));
-    }
-
-    @Test
-    public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnActiveUserDisconnect() {
-        String deviceId = UUID.randomUUID().toString();
-        when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
-                Collections.singletonList(deviceId));
-        AssociatedDevice device = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
-                TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-        when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
-                Collections.singletonList(device));
-        mConnectedDeviceManager.addConnectedDevice(deviceId);
-        clearInvocations(mMockCarBluetoothManager);
-        mConnectedDeviceManager.removeConnectedDevice(deviceId);
-        verify(mMockCarBluetoothManager, timeout(1000))
-                .connectToDevice(eq(UUID.fromString(deviceId)));
-    }
-
-    @Test
-    public void removeConnectedDevice_startsAdvertisingForActiveUserDeviceOnLastDeviceDisconnect() {
-        String deviceId = UUID.randomUUID().toString();
-        String userDeviceId = UUID.randomUUID().toString();
-        when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
-                Collections.singletonList(userDeviceId));
-        AssociatedDevice userDevice = new AssociatedDevice(userDeviceId, TEST_DEVICE_ADDRESS,
-                TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-        when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
-                Collections.singletonList(userDevice));
-        mConnectedDeviceManager.addConnectedDevice(deviceId);
-        clearInvocations(mMockCarBluetoothManager);
-        mConnectedDeviceManager.removeConnectedDevice(deviceId);
-        verify(mMockCarBluetoothManager, timeout(1000)).connectToDevice(
-                eq(UUID.fromString(userDeviceId)));
-    }
-
-    @Test
-    public void removeConnectedDevice_doesNotAdvertiseForNonActiveUserDeviceNotLastDevice() {
-        String deviceId = UUID.randomUUID().toString();
-        String userDeviceId = UUID.randomUUID().toString();
-        when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
-                Collections.singletonList(userDeviceId));
-        AssociatedDevice userDevice = new AssociatedDevice(userDeviceId, TEST_DEVICE_ADDRESS,
-                TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-        when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
-                Collections.singletonList(userDevice));
-        mConnectedDeviceManager.addConnectedDevice(deviceId);
-        mConnectedDeviceManager.addConnectedDevice(userDeviceId);
-        clearInvocations(mMockCarBluetoothManager);
-        mConnectedDeviceManager.removeConnectedDevice(deviceId);
-        verify(mMockCarBluetoothManager, timeout(1000).times(0))
-                .connectToDevice(any());
-    }
-
-    @Test
-    public void removeActiveUserAssociatedDevice_deletesAssociatedDeviceFromStorage() {
-        String deviceId = UUID.randomUUID().toString();
-        mConnectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
-        verify(mMockStorage).removeAssociatedDeviceForActiveUser(deviceId);
-    }
-
-    @Test
-    public void removeActiveUserAssociatedDevice_disconnectsIfConnected() {
-        String deviceId = connectNewDevice();
-        mConnectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
-        verify(mMockCarBluetoothManager).disconnectDevice(deviceId);
-    }
-
-    @Test
-    public void enableAssociatedDeviceConnection_enableDeviceConnectionInStorage() {
-        String deviceId = UUID.randomUUID().toString();
-        mConnectedDeviceManager.enableAssociatedDeviceConnection(deviceId);
-        verify(mMockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, true);
-    }
-
-    @Test
-    public void disableAssociatedDeviceConnection_disableDeviceConnectionInStorage() {
-        String deviceId = UUID.randomUUID().toString();
-        mConnectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
-        verify(mMockStorage).updateAssociatedDeviceConnectionEnabled(deviceId, false);
-    }
-
-    @Test
-    public void disableAssociatedDeviceConnection_disconnectsIfConnected() {
-        String deviceId = connectNewDevice();
-        mConnectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
-        verify(mMockCarBluetoothManager).disconnectDevice(deviceId);
-    }
-
-    @Test
-    public void onMessageReceived_deliversMessageIfDelegateIsNull() throws InterruptedException {
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        Semaphore semaphore = new Semaphore(0);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
-        mConnectedDeviceManager.setMessageDeliveryDelegate(null);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-        assertThat(tryAcquire(semaphore)).isTrue();
-    }
-
-    @Test
-    public void onMessageReceived_deliversMessageIfDelegateAccepts() throws InterruptedException {
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        Semaphore semaphore = new Semaphore(0);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
-        MessageDeliveryDelegate delegate = device -> true;
-        mConnectedDeviceManager.setMessageDeliveryDelegate(delegate);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-        assertThat(tryAcquire(semaphore)).isTrue();
-    }
-
-    @Test
-    public void onMessageReceived_doesNotDeliverMessageIfDelegateRejects()
-            throws InterruptedException {
-        connectNewDevice();
-        ConnectedDevice connectedDevice =
-                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
-        Semaphore semaphore = new Semaphore(0);
-        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
-        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
-                deviceCallback, mCallbackExecutor);
-        DeviceMessage message = new DeviceMessage(mRecipientId, false, new byte[10]);
-        MessageDeliveryDelegate delegate = device -> false;
-        mConnectedDeviceManager.setMessageDeliveryDelegate(delegate);
-        mConnectedDeviceManager.onMessageReceived(connectedDevice.getDeviceId(), message);
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @NonNull
-    private String connectNewDevice() {
-        String deviceId = UUID.randomUUID().toString();
-        AssociatedDevice device = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
-                TEST_DEVICE_NAME, /* isConnectionEnabled= */ true);
-        mUserDeviceIds.add(deviceId);
-        mUserDevices.add(device);
-        mConnectedDeviceManager.addConnectedDevice(deviceId);
-        return deviceId;
-    }
-
-    @NonNull
-    private ConnectionCallback createConnectionCallback(@NonNull final Semaphore semaphore) {
-        return spy(new ConnectionCallback() {
-            @Override
-            public void onDeviceConnected(ConnectedDevice device) {
-                semaphore.release();
-            }
-
-            @Override
-            public void onDeviceDisconnected(ConnectedDevice device) {
-                semaphore.release();
-            }
-        });
-    }
-
-    @NonNull
-    private DeviceCallback createDeviceCallback(@NonNull final Semaphore semaphore) {
-        return spy(new DeviceCallback() {
-            @Override
-            public void onSecureChannelEstablished(ConnectedDevice device) {
-                semaphore.release();
-            }
-
-            @Override
-            public void onMessageReceived(ConnectedDevice device, byte[] message) {
-                semaphore.release();
-            }
-
-            @Override
-            public void onDeviceError(ConnectedDevice device, int error) {
-                semaphore.release();
-            }
-        });
-    }
-
-    @NonNull
-    private DeviceAssociationCallback createDeviceAssociationCallback(
-            @NonNull final Semaphore semaphore) {
-        return spy(new DeviceAssociationCallback() {
-            @Override
-            public void onAssociatedDeviceAdded(AssociatedDevice device) {
-                semaphore.release();
-            }
-
-            @Override
-            public void onAssociatedDeviceRemoved(
-                    AssociatedDevice device) {
-                semaphore.release();
-            }
-
-            @Override
-            public void onAssociatedDeviceUpdated(AssociatedDevice device) {
-                semaphore.release();
-            }
-        });
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/AssociationSecureChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/AssociationSecureChannelTest.java
deleted file mode 100644
index 118eca9..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/AssociationSecureChannelTest.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.car.encryptionrunner.EncryptionRunner;
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.FakeEncryptionRunner;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-import com.android.car.connecteddevice.connection.ble.BleDeviceMessageStream;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.util.UUID;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public final class AssociationSecureChannelTest {
-    private static final UUID CLIENT_DEVICE_ID =
-            UUID.fromString("a5645523-3280-410a-90c1-582a6c6f4969");
-    private static final UUID SERVER_DEVICE_ID =
-            UUID.fromString("a29f0c74-2014-4b14-ac02-be6ed15b545a");
-    private static final byte[] CLIENT_SECRET = ByteUtils.randomBytes(32);
-
-    @Mock
-    private BleDeviceMessageStream mStreamMock;
-    @Mock
-    private ConnectedDeviceStorage mStorageMock;
-    @Mock
-    private AssociationSecureChannel.ShowVerificationCodeListener mShowVerificationCodeListenerMock;
-    private MockitoSession mMockitoSession;
-
-    private AssociationSecureChannel mChannel;
-    private DeviceMessageStream.MessageReceivedListener mMessageReceivedListener;
-
-    @Before
-    public void setUp() {
-        mMockitoSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-        when(mStorageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
-    }
-
-    @After
-    public void tearDown() {
-        if (mMockitoSession != null) {
-            mMockitoSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void testEncryptionHandshake_Association() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        ChannelCallback callbackSpy = spy(new ChannelCallback(semaphore));
-        setupAssociationSecureChannel(callbackSpy, EncryptionRunnerFactory::newFakeRunner);
-        ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
-        ArgumentCaptor<DeviceMessage> messageCaptor =
-                ArgumentCaptor.forClass(DeviceMessage.class);
-
-        initHandshakeMessage();
-        verify(mStreamMock).writeMessage(messageCaptor.capture(), any());
-        byte[] response = messageCaptor.getValue().getMessage();
-        assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE.getBytes());
-
-        respondToContinueMessage();
-        verify(mShowVerificationCodeListenerMock).showVerificationCode(anyString());
-
-        mChannel.notifyOutOfBandAccepted();
-        sendDeviceId();
-        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
-        verify(callbackSpy).onDeviceIdReceived(deviceIdCaptor.capture());
-        verify(mStreamMock, times(2)).writeMessage(messageCaptor.capture(), any());
-        byte[] deviceIdMessage = messageCaptor.getValue().getMessage();
-        assertThat(deviceIdMessage).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
-        assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
-        verify(mStorageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
-        verify(mStorageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
-
-        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
-        verify(callbackSpy).onSecureChannelEstablished();
-    }
-
-    @Test
-    public void testEncryptionHandshake_Association_wrongInitHandshakeMessage()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        ChannelCallback callbackSpy = spy(new ChannelCallback(semaphore));
-        setupAssociationSecureChannel(callbackSpy, EncryptionRunnerFactory::newFakeRunner);
-
-        // Wrong init handshake message
-        respondToContinueMessage();
-        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
-        verify(callbackSpy).onEstablishSecureChannelFailure(
-                eq(SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE)
-        );
-    }
-
-    @Test
-    public void testEncryptionHandshake_Association_wrongRespondToContinueMessage()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        ChannelCallback callbackSpy = spy(new ChannelCallback(semaphore));
-        setupAssociationSecureChannel(callbackSpy, EncryptionRunnerFactory::newFakeRunner);
-
-        initHandshakeMessage();
-
-        // Wrong respond to continue message
-        initHandshakeMessage();
-        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
-        verify(callbackSpy).onEstablishSecureChannelFailure(
-                eq(SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE)
-        );
-    }
-
-    private void setupAssociationSecureChannel(ChannelCallback callback,
-            EncryptionRunnerProvider encryptionRunnerProvider) {
-        mChannel = new AssociationSecureChannel(mStreamMock, mStorageMock,
-                encryptionRunnerProvider.getEncryptionRunner());
-        mChannel.registerCallback(callback);
-        mChannel.setShowVerificationCodeListener(mShowVerificationCodeListenerMock);
-        ArgumentCaptor<DeviceMessageStream.MessageReceivedListener> listenerCaptor =
-                ArgumentCaptor.forClass(DeviceMessageStream.MessageReceivedListener.class);
-        verify(mStreamMock).setMessageReceivedListener(listenerCaptor.capture());
-        mMessageReceivedListener = listenerCaptor.getValue();
-    }
-
-    private void sendDeviceId() {
-        DeviceMessage message = new DeviceMessage(
-                /* recipient= */ null,
-                /* isMessageEncrypted= */ true,
-                ByteUtils.concatByteArrays(ByteUtils.uuidToBytes(CLIENT_DEVICE_ID), CLIENT_SECRET)
-        );
-        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
-    }
-
-    private void initHandshakeMessage() {
-        DeviceMessage message = new DeviceMessage(
-                /* recipient= */ null,
-                /* isMessageEncrypted= */ false,
-                FakeEncryptionRunner.INIT.getBytes()
-        );
-        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
-    }
-
-    private void respondToContinueMessage() {
-        DeviceMessage message = new DeviceMessage(
-                /* recipient= */ null,
-                /* isMessageEncrypted= */ false,
-                FakeEncryptionRunner.CLIENT_RESPONSE.getBytes()
-        );
-        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
-    }
-
-    /**
-     * Add the thread control logic into {@link SecureChannel.Callback} only for spy purpose.
-     *
-     * <p>The callback will release the semaphore which hold by one test when this callback
-     * is called, telling the test that it can verify certain behaviors which will only occurred
-     * after the callback is notified. This is needed mainly because of the callback is notified
-     * in a different thread.
-     */
-    private static class ChannelCallback implements SecureChannel.Callback {
-        private final Semaphore mSemaphore;
-
-        ChannelCallback(Semaphore semaphore) {
-            mSemaphore = semaphore;
-        }
-
-        @Override
-        public void onSecureChannelEstablished() {
-            mSemaphore.release();
-        }
-
-        @Override
-        public void onEstablishSecureChannelFailure(int error) {
-            mSemaphore.release();
-        }
-
-        @Override
-        public void onMessageReceived(DeviceMessage deviceMessage) {
-            mSemaphore.release();
-        }
-
-        @Override
-        public void onMessageReceivedError(Exception exception) {
-            mSemaphore.release();
-        }
-
-        @Override
-        public void onDeviceIdReceived(String deviceId) {
-            mSemaphore.release();
-        }
-    }
-
-    interface EncryptionRunnerProvider {
-        EncryptionRunner getEncryptionRunner();
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/DeviceMessageStreamTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/DeviceMessageStreamTest.java
deleted file mode 100644
index 661bece..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/DeviceMessageStreamTest.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.StreamProtos.DeviceMessageProto.Message;
-import static com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-import static com.android.car.connecteddevice.StreamProtos.PacketProto.Packet;
-import static com.android.car.connecteddevice.connection.DeviceMessageStream.MessageReceivedErrorListener;
-import static com.android.car.connecteddevice.connection.DeviceMessageStream.MessageReceivedListener;
-
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.util.ByteUtils;
-import com.android.car.protobuf.ByteString;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class DeviceMessageStreamTest {
-
-    private static final int WRITE_SIZE = 500;
-
-    private DeviceMessageStream mStream;
-
-    @Before
-    public void setup() {
-        mStream = spy(new DeviceMessageStream(WRITE_SIZE) {
-            @Override
-            protected void send(byte[] data) { }
-        });
-    }
-
-    @Test
-    public void processPacket_notifiesWithEntireMessageForSinglePacketMessage()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        MessageReceivedListener listener = createMessageReceivedListener(semaphore);
-        mStream.setMessageReceivedListener(listener);
-        byte[] data = ByteUtils.randomBytes(5);
-        processMessage(data);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-        verify(listener).onMessageReceived(messageCaptor.capture(), any());
-    }
-
-    @Test
-    public void processPacket_notifiesWithEntireMessageForMultiPacketMessage()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        MessageReceivedListener listener = createMessageReceivedListener(semaphore);
-        mStream.setMessageReceivedListener(listener);
-        byte[] data = ByteUtils.randomBytes(750);
-        processMessage(data);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-        verify(listener).onMessageReceived(messageCaptor.capture(), any());
-        assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
-    }
-
-    @Test
-    public void processPacket_receivingMultipleMessagesInParallelParsesSuccessfully()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        MessageReceivedListener listener = createMessageReceivedListener(semaphore);
-        mStream.setMessageReceivedListener(listener);
-        byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
-        List<Packet> packets1 = createPackets(data);
-        List<Packet> packets2 = createPackets(data);
-
-        for (int i = 0; i < packets1.size(); i++) {
-            mStream.processPacket(packets1.get(i));
-            if (i == packets1.size() - 1) {
-                break;
-            }
-            mStream.processPacket(packets2.get(i));
-        }
-        assertThat(tryAcquire(semaphore)).isTrue();
-        ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-        verify(listener).onMessageReceived(messageCaptor.capture(), any());
-        assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
-
-        semaphore = new Semaphore(0);
-        listener = createMessageReceivedListener(semaphore);
-        mStream.setMessageReceivedListener(listener);
-        mStream.processPacket(packets2.get(packets2.size() - 1));
-        verify(listener).onMessageReceived(messageCaptor.capture(), any());
-        assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
-    }
-
-    @Test
-    public void processPacket_doesNotNotifyOfNewMessageIfNotAllPacketsReceived()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        mStream.setMessageReceivedListener(createMessageReceivedListener(semaphore));
-        mStream.setMessageReceivedErrorListener(createMessageReceivedErrorListener(semaphore));
-        byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5));
-        List<Packet> packets = createPackets(data);
-        for (int i = 0; i < packets.size() - 1; i++) {
-            mStream.processPacket(packets.get(i));
-        }
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void processPacket_ignoresDuplicatePacket() {
-        Semaphore semaphore = new Semaphore(0);
-        byte[] data = ByteUtils.randomBytes((int) (WRITE_SIZE * 2.5));
-        MessageReceivedListener listener = createMessageReceivedListener(semaphore);
-        mStream.setMessageReceivedListener(listener);
-        ArgumentCaptor<DeviceMessage> messageCaptor = ArgumentCaptor.forClass(DeviceMessage.class);
-        List<Packet> packets = createPackets(data);
-        for (int i = 0; i < packets.size(); i++) {
-            mStream.processPacket(packets.get(i));
-            mStream.processPacket(packets.get(i)); // Process each packet twice.
-        }
-        verify(listener).onMessageReceived(messageCaptor.capture(), any());
-        assertThat(Arrays.equals(data, messageCaptor.getValue().getMessage())).isTrue();
-    }
-
-    @Test
-    public void processPacket_packetBeforeExpectedRangeNotifiesMessageError()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        mStream.setMessageReceivedErrorListener(createMessageReceivedErrorListener(semaphore));
-        List<Packet> packets = createPackets(ByteUtils.randomBytes((int) (WRITE_SIZE * 2.5)));
-        mStream.processPacket(packets.get(0));
-        mStream.processPacket(packets.get(1));
-        mStream.processPacket(packets.get(0));
-        assertThat(tryAcquire(semaphore)).isTrue();
-    }
-
-    @Test
-    public void processPacket_packetAfterExpectedNotifiesMessageError()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        mStream.setMessageReceivedErrorListener(createMessageReceivedErrorListener(semaphore));
-        List<Packet> packets = createPackets(ByteUtils.randomBytes((int) (WRITE_SIZE * 1.5)));
-        mStream.processPacket(packets.get(1));
-        assertThat(tryAcquire(semaphore)).isTrue();
-    }
-
-    @NonNull
-    private List<Packet> createPackets(byte[] data) {
-        try {
-            Message message = Message.newBuilder()
-                    .setPayload(ByteString.copyFrom(data))
-                    .setOperation(OperationType.CLIENT_MESSAGE)
-                    .build();
-            return PacketFactory.makePackets(message.toByteArray(),
-                    ThreadLocalRandom.current().nextInt(), WRITE_SIZE);
-        } catch (Exception e) {
-            assertWithMessage("Uncaught exception while making packets.").fail();
-            return new ArrayList<>();
-        }
-    }
-
-    private void processMessage(byte[] data) {
-        List<Packet> packets = createPackets(data);
-        for (Packet packet : packets) {
-            mStream.processPacket(packet);
-        }
-    }
-
-    private boolean tryAcquire(@NonNull Semaphore semaphore) throws InterruptedException {
-        return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
-    }
-
-    @NonNull
-    private MessageReceivedListener createMessageReceivedListener(@NonNull Semaphore semaphore) {
-        return spy((deviceMessage, operationType) -> semaphore.release());
-    }
-
-    @NonNull
-    private MessageReceivedErrorListener createMessageReceivedErrorListener(
-            @NonNull Semaphore semaphore) {
-        return exception -> semaphore.release();
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannelTest.java
deleted file mode 100644
index 3ad2a8d..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/OobAssociationSecureChannelTest.java
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.FakeEncryptionRunner;
-
-import com.android.car.connecteddevice.connection.ble.BleDeviceMessageStream;
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.util.UUID;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-
-public class OobAssociationSecureChannelTest {
-    private static final UUID CLIENT_DEVICE_ID =
-            UUID.fromString("a5645523-3280-410a-90c1-582a6c6f4969");
-
-    private static final UUID SERVER_DEVICE_ID =
-            UUID.fromString("a29f0c74-2014-4b14-ac02-be6ed15b545a");
-
-    private static final byte[] CLIENT_SECRET = ByteUtils.randomBytes(32);
-
-    @Mock
-    private BleDeviceMessageStream mStreamMock;
-
-    @Mock
-    private ConnectedDeviceStorage mStorageMock;
-
-    @Mock
-    private OobConnectionManager mOobConnectionManagerMock;
-
-    private OobAssociationSecureChannel mChannel;
-
-    private BleDeviceMessageStream.MessageReceivedListener mMessageReceivedListener;
-
-    private MockitoSession mMockitoSession;
-
-    @Before
-    public void setUp() {
-        mMockitoSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-        when(mStorageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
-    }
-
-    @After
-    public void tearDown() {
-        if (mMockitoSession != null) {
-            mMockitoSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void testEncryptionHandshake_oobAssociation() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        ChannelCallback
-                callbackSpy = spy(new ChannelCallback(semaphore));
-        setupOobAssociationSecureChannel(callbackSpy);
-        ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
-        ArgumentCaptor<DeviceMessage> messageCaptor =
-                ArgumentCaptor.forClass(DeviceMessage.class);
-
-        initHandshakeMessage();
-        verify(mStreamMock).writeMessage(messageCaptor.capture(), any());
-        byte[] response = messageCaptor.getValue().getMessage();
-        assertThat(response).isEqualTo(FakeEncryptionRunner.INIT_RESPONSE.getBytes());
-        reset(mStreamMock);
-        respondToContinueMessage();
-        verify(mStreamMock).writeMessage(messageCaptor.capture(), any());
-        byte[] oobCodeResponse = messageCaptor.getValue().getMessage();
-        assertThat(oobCodeResponse).isEqualTo(FakeEncryptionRunner.VERIFICATION_CODE.getBytes());
-        respondToOobCode();
-        sendDeviceId();
-        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
-        verify(callbackSpy).onDeviceIdReceived(deviceIdCaptor.capture());
-        verify(mStreamMock, times(2)).writeMessage(messageCaptor.capture(), any());
-        byte[] deviceIdMessage = messageCaptor.getValue().getMessage();
-        assertThat(deviceIdMessage).isEqualTo(ByteUtils.uuidToBytes(SERVER_DEVICE_ID));
-        assertThat(deviceIdCaptor.getValue()).isEqualTo(CLIENT_DEVICE_ID.toString());
-        verify(mStorageMock).saveEncryptionKey(eq(CLIENT_DEVICE_ID.toString()), any());
-        verify(mStorageMock).saveChallengeSecret(CLIENT_DEVICE_ID.toString(), CLIENT_SECRET);
-
-        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
-        verify(callbackSpy).onSecureChannelEstablished();
-    }
-
-    private void setupOobAssociationSecureChannel(ChannelCallback callback) {
-        mChannel = new OobAssociationSecureChannel(mStreamMock, mStorageMock,
-                mOobConnectionManagerMock, EncryptionRunnerFactory.newOobFakeRunner());
-        mChannel.registerCallback(callback);
-        ArgumentCaptor<BleDeviceMessageStream.MessageReceivedListener> listenerCaptor =
-                ArgumentCaptor.forClass(BleDeviceMessageStream.MessageReceivedListener.class);
-        verify(mStreamMock).setMessageReceivedListener(listenerCaptor.capture());
-        mMessageReceivedListener = listenerCaptor.getValue();
-        try {
-            when(mOobConnectionManagerMock.encryptVerificationCode(any()))
-                    .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE.getBytes());
-        } catch (InvalidAlgorithmParameterException | BadPaddingException | InvalidKeyException
-                | IllegalBlockSizeException e) {
-        }
-        try {
-            when(mOobConnectionManagerMock.decryptVerificationCode(any()))
-                    .thenReturn(FakeEncryptionRunner.VERIFICATION_CODE.getBytes());
-        } catch (InvalidAlgorithmParameterException | BadPaddingException | InvalidKeyException
-                | IllegalBlockSizeException e) {
-        }
-    }
-
-    private void sendDeviceId() {
-        DeviceMessage message = new DeviceMessage(
-                /* recipient= */ null,
-                /* isMessageEncrypted= */ true,
-                ByteUtils.concatByteArrays(ByteUtils.uuidToBytes(CLIENT_DEVICE_ID), CLIENT_SECRET)
-        );
-        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
-    }
-
-    private void initHandshakeMessage() {
-        DeviceMessage message = new DeviceMessage(
-                /* recipient= */ null,
-                /* isMessageEncrypted= */ false,
-                FakeEncryptionRunner.INIT.getBytes()
-        );
-        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
-    }
-
-    private void respondToContinueMessage() {
-        DeviceMessage message = new DeviceMessage(
-                /* recipient= */ null,
-                /* isMessageEncrypted= */ false,
-                FakeEncryptionRunner.CLIENT_RESPONSE.getBytes()
-        );
-        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
-    }
-
-    private void respondToOobCode() {
-        DeviceMessage message = new DeviceMessage(
-                /* recipient= */ null,
-                /* isMessageEncrypted= */ false,
-                FakeEncryptionRunner.VERIFICATION_CODE.getBytes()
-        );
-        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
-    }
-
-    /**
-     * Add the thread control logic into {@link SecureChannel.Callback} only for spy purpose.
-     *
-     * <p>The callback will release the semaphore which hold by one test when this callback
-     * is called, telling the test that it can verify certain behaviors which will only occurred
-     * after the callback is notified. This is needed mainly because of the callback is notified
-     * in a different thread.
-     */
-    private static class ChannelCallback implements SecureChannel.Callback {
-        private final Semaphore mSemaphore;
-
-        ChannelCallback(Semaphore semaphore) {
-            mSemaphore = semaphore;
-        }
-
-        @Override
-        public void onSecureChannelEstablished() {
-            mSemaphore.release();
-        }
-
-        @Override
-        public void onEstablishSecureChannelFailure(int error) {
-            mSemaphore.release();
-        }
-
-        @Override
-        public void onMessageReceived(DeviceMessage deviceMessage) {
-            mSemaphore.release();
-        }
-
-        @Override
-        public void onMessageReceivedError(Exception exception) {
-            mSemaphore.release();
-        }
-
-        @Override
-        public void onDeviceIdReceived(String deviceId) {
-            mSemaphore.release();
-        }
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/PacketFactoryTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/PacketFactoryTest.java
deleted file mode 100644
index d5fdc03..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/PacketFactoryTest.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.StreamProtos.PacketProto.Packet;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.ByteArrayOutputStream;
-import java.util.List;
-import java.util.Random;
-
-@RunWith(AndroidJUnit4.class)
-public class PacketFactoryTest {
-    @Test
-    public void testGetHeaderSize() {
-        // 1 byte to encode the ID, 1 byte for the field number.
-        int messageId = 1;
-        int messageIdEncodingSize = 2;
-
-        // 1 byte for the payload size, 1 byte for the field number.
-        int payloadSize = 2;
-        int payloadSizeEncodingSize = 2;
-
-        // 1 byte for total packets, 1 byte for field number.
-        int totalPackets = 5;
-        int totalPacketsEncodingSize = 2;
-
-        // Packet number if a fixed32, so 4 bytes + 1 byte for field number.
-        int packetNumberEncodingSize = 5;
-
-        int expectedHeaderSize = messageIdEncodingSize + payloadSizeEncodingSize
-                + totalPacketsEncodingSize + packetNumberEncodingSize;
-
-        assertThat(PacketFactory.getPacketHeaderSize(totalPackets, messageId, payloadSize))
-                .isEqualTo(expectedHeaderSize);
-    }
-
-    @Test
-    public void testGetTotalPackets_withVarintSize1_returnsCorrectPackets()
-            throws PacketFactoryException {
-        int messageId = 1;
-        int maxSize = 49;
-        int payloadSize = 100;
-
-        // This leaves us 40 bytes to use for the payload and its encoding size. Assuming a varint
-        // of size 1 means it takes 2 bytes to encode its value. This leaves 38 bytes for the
-        // payload. ceil(payloadSize/38) gives the total packets.
-        int expectedTotalPackets = 3;
-
-        assertThat(PacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
-                .isEqualTo(expectedTotalPackets);
-    }
-
-    @Test
-    public void testGetTotalPackets_withVarintSize2_returnsCorrectPackets()
-            throws PacketFactoryException {
-        int messageId = 1;
-        int maxSize = 49;
-        int payloadSize = 6000;
-
-        // This leaves us 40 bytes to use for the payload and its encoding size. Assuming a varint
-        // of size 2 means it takes 3 bytes to encode its value. This leaves 37 bytes for the
-        // payload. ceil(payloadSize/37) gives the total packets.
-        int expectedTotalPackets = 163;
-
-        assertThat(PacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
-                .isEqualTo(expectedTotalPackets);
-    }
-
-    @Test
-    public void testGetTotalPackets_withVarintSize3_returnsCorrectPackets()
-            throws PacketFactoryException {
-        int messageId = 1;
-        int maxSize = 49;
-        int payloadSize = 1000000;
-
-        // This leaves us 40 bytes to use for the payload and its encoding size. Assuming a varint
-        // of size 3 means it takes 4 bytes to encode its value. This leaves 36 bytes for the
-        // payload. ceil(payloadSize/36) gives the total packets.
-        int expectedTotalPackets = 27778;
-
-        assertThat(PacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
-                .isEqualTo(expectedTotalPackets);
-    }
-
-    @Test
-    public void testGetTotalPackets_withVarintSize4_returnsCorrectPackets()
-            throws PacketFactoryException {
-        int messageId = 1;
-        int maxSize = 49;
-        int payloadSize = 178400320;
-
-        // This leaves us 40 bytes to use for the payload and its encoding size. Assuming a varint
-        // of size 4 means it takes 5 bytes to encode its value. This leaves 35 bytes for the
-        // payload. ceil(payloadSize/35) gives the total packets.
-        int expectedTotalPackets = 5097152;
-
-        assertThat(PacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
-                .isEqualTo(expectedTotalPackets);
-    }
-
-    @Test
-    public void testMakePackets_correctlyChunksPayload() throws Exception {
-        // Payload of size 100, but maxSize of 1000 to ensure it fits.
-        byte[] payload = makePayload(/* length= */ 100);
-        int maxSize = 1000;
-
-        List<Packet> packets =
-                PacketFactory.makePackets(payload, /* messageId= */ 1, maxSize);
-
-        assertThat(packets).hasSize(1);
-
-        ByteArrayOutputStream reconstructedPayload = new ByteArrayOutputStream();
-
-        // Combine together all the payloads within the BlePackets.
-        for (Packet packet : packets) {
-            reconstructedPayload.write(packet.getPayload().toByteArray());
-        }
-
-        assertThat(reconstructedPayload.toByteArray()).isEqualTo(payload);
-    }
-
-    @Test
-    public void testMakePackets_correctlyChunksSplitPayload() throws Exception {
-        // Payload size of 10000 but max size of 50 to ensure the payload is split.
-        byte[] payload = makePayload(/* length= */ 10000);
-        int maxSize = 50;
-
-        List<Packet> packets =
-                PacketFactory.makePackets(payload, /* messageId= */ 1, maxSize);
-
-        assertThat(packets.size()).isGreaterThan(1);
-
-        ByteArrayOutputStream reconstructedPayload = new ByteArrayOutputStream();
-
-        // Combine together all the payloads within the BlePackets.
-        for (Packet packet : packets) {
-            reconstructedPayload.write(packet.getPayload().toByteArray());
-        }
-
-        assertThat(reconstructedPayload.toByteArray()).isEqualTo(payload);
-    }
-
-    /** Creates a byte array of the given length, populated with random bytes. */
-    private byte[] makePayload(int length) {
-        byte[] payload = new byte[length];
-        new Random().nextBytes(payload);
-        return payload;
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/SecureChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/SecureChannelTest.java
deleted file mode 100644
index ce56ebb..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/SecureChannelTest.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection;
-
-import static com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType.CLIENT_MESSAGE;
-import static com.android.car.connecteddevice.StreamProtos.OperationProto.OperationType.ENCRYPTION_HANDSHAKE;
-import static com.android.car.connecteddevice.connection.SecureChannel.CHANNEL_ERROR_INVALID_HANDSHAKE;
-import static com.android.car.connecteddevice.connection.SecureChannel.Callback;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.car.encryptionrunner.EncryptionRunnerFactory;
-import android.car.encryptionrunner.FakeEncryptionRunner;
-import android.car.encryptionrunner.HandshakeException;
-import android.car.encryptionrunner.Key;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.connection.ble.BleDeviceMessageStream;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.security.NoSuchAlgorithmException;
-import java.security.SignatureException;
-import java.util.UUID;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class SecureChannelTest {
-
-    @Mock private BleDeviceMessageStream mMockStream;
-
-    @Mock private Key mKey = spy(new Key() {
-        @Override
-        public byte[] asBytes() {
-            return new byte[0];
-        }
-
-        @Override
-        public byte[] encryptData(byte[] data) {
-            return data;
-        }
-
-        @Override
-        public byte[] decryptData(byte[] encryptedData) throws SignatureException {
-            return encryptedData;
-        }
-
-        @Override
-        public byte[] getUniqueSession() throws NoSuchAlgorithmException {
-            return new byte[0];
-        }
-    });
-
-    private MockitoSession mMockitoSession;
-
-    private SecureChannel mSecureChannel;
-
-    @Before
-    public void setUp() throws SignatureException {
-        mMockitoSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-
-        mSecureChannel = new SecureChannel(mMockStream,
-                EncryptionRunnerFactory.newFakeRunner()) {
-            @Override
-            void processHandshake(byte[] message) { }
-        };
-        mSecureChannel.setEncryptionKey(mKey);
-    }
-
-    @After
-    public void tearDown() {
-        if (mMockitoSession != null) {
-            mMockitoSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void processMessage_doesNothingForUnencryptedMessage() throws SignatureException {
-        byte[] payload = ByteUtils.randomBytes(10);
-        DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ false,
-                payload);
-        mSecureChannel.processMessage(message);
-        assertThat(message.getMessage()).isEqualTo(payload);
-        verify(mKey, times(0)).decryptData(any());
-    }
-
-    @Test
-    public void processMessage_decryptsEncryptedMessage() throws SignatureException {
-        byte[] payload = ByteUtils.randomBytes(10);
-        DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ true,
-                payload);
-        mSecureChannel.processMessage(message);
-        verify(mKey).decryptData(any());
-    }
-
-    @Test
-    public void processMessage_onMessageReceivedErrorForEncryptedMessageWithNoKey()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ true,
-                ByteUtils.randomBytes(10));
-
-        mSecureChannel.setEncryptionKey(null);
-        mSecureChannel.registerCallback(new Callback() {
-            @Override
-            public void onMessageReceivedError(Exception exception) {
-                semaphore.release();
-            }
-        });
-        mSecureChannel.processMessage(message);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        assertThat(message.getMessage()).isNull();
-    }
-
-    @Test
-    public void onMessageReceived_onEstablishSecureChannelFailureBadHandshakeMessage()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ true,
-                ByteUtils.randomBytes(10));
-
-        mSecureChannel.setEncryptionKey(null);
-        mSecureChannel.registerCallback(new Callback() {
-            @Override
-            public void onEstablishSecureChannelFailure(int error) {
-                assertThat(error).isEqualTo(CHANNEL_ERROR_INVALID_HANDSHAKE);
-                semaphore.release();
-            }
-        });
-        mSecureChannel.onMessageReceived(message, ENCRYPTION_HANDSHAKE);
-        assertThat(tryAcquire(semaphore)).isTrue();
-    }
-
-    @Test
-    public void onMessageReceived_onMessageReceivedNotIssuedForNullMessage()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ false,
-                /* message= */ null);
-
-        mSecureChannel.registerCallback(new Callback() {
-            @Override
-            public void onMessageReceived(DeviceMessage message) {
-                semaphore.release();
-            }
-        });
-        mSecureChannel.onMessageReceived(message, CLIENT_MESSAGE);
-        assertThat(tryAcquire(semaphore)).isFalse();
-    }
-
-    @Test
-    public void onMessageReceived_processHandshakeExceptionIssuesSecureChannelFailureCallback()
-            throws InterruptedException {
-        SecureChannel secureChannel = new SecureChannel(mMockStream,
-                EncryptionRunnerFactory.newFakeRunner()) {
-            @Override
-            void processHandshake(byte[] message) throws HandshakeException {
-                FakeEncryptionRunner.throwHandshakeException("test");
-            }
-        };
-        Semaphore semaphore = new Semaphore(0);
-        secureChannel.registerCallback(new Callback() {
-            @Override
-            public void onEstablishSecureChannelFailure(int error) {
-                semaphore.release();
-            }
-        });
-        DeviceMessage message = new DeviceMessage(UUID.randomUUID(), /* isEncrypted= */ true,
-                /* message= */ ByteUtils.randomBytes(10));
-
-        secureChannel.onMessageReceived(message, ENCRYPTION_HANDSHAKE);
-        assertThat(tryAcquire(semaphore)).isTrue();
-    }
-
-    private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
-        return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java
deleted file mode 100644
index b4a229e..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/ble/CarBlePeripheralManagerTest.java
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.ble;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.le.AdvertiseCallback;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertiseSettings;
-import android.os.ParcelUuid;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.AssociationSecureChannel;
-import com.android.car.connecteddevice.connection.SecureChannel;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.oob.OobConnectionManager;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.time.Duration;
-import java.util.UUID;
-
-@RunWith(AndroidJUnit4.class)
-public class CarBlePeripheralManagerTest {
-    private static final UUID ASSOCIATION_SERVICE_UUID = UUID.randomUUID();
-    private static final UUID RECONNECT_SERVICE_UUID = UUID.randomUUID();
-    private static final UUID RECONNECT_DATA_UUID = UUID.randomUUID();
-    private static final UUID WRITE_UUID = UUID.randomUUID();
-    private static final UUID READ_UUID = UUID.randomUUID();
-    private static final int DEVICE_NAME_LENGTH_LIMIT = 8;
-    private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB";
-    private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID();
-    private static final String TEST_VERIFICATION_CODE = "000000";
-    private static final String TEST_ENCRYPTED_VERIFICATION_CODE = "12345";
-    private static final Duration RECONNECT_ADVERTISEMENT_DURATION = Duration.ofSeconds(2);
-    private static final int DEFAULT_MTU_SIZE = 23;
-
-    @Mock
-    private BlePeripheralManager mMockPeripheralManager;
-    @Mock
-    private ConnectedDeviceStorage mMockStorage;
-    @Mock
-    private OobConnectionManager mMockOobConnectionManager;
-    @Mock
-    private AssociationCallback mAssociationCallback;
-
-    private CarBlePeripheralManager mCarBlePeripheralManager;
-
-    private MockitoSession mMockitoSession;
-
-    @Before
-    public void setUp() throws Exception {
-        mMockitoSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-        mCarBlePeripheralManager = new CarBlePeripheralManager(mMockPeripheralManager, mMockStorage,
-                ASSOCIATION_SERVICE_UUID, RECONNECT_SERVICE_UUID,
-                RECONNECT_DATA_UUID, WRITE_UUID, READ_UUID, RECONNECT_ADVERTISEMENT_DURATION,
-                DEFAULT_MTU_SIZE);
-
-        when(mMockOobConnectionManager.encryptVerificationCode(
-                TEST_VERIFICATION_CODE.getBytes())).thenReturn(
-                TEST_ENCRYPTED_VERIFICATION_CODE.getBytes());
-        when(mMockOobConnectionManager.decryptVerificationCode(
-                TEST_ENCRYPTED_VERIFICATION_CODE.getBytes())).thenReturn(
-                TEST_VERIFICATION_CODE.getBytes());
-        mCarBlePeripheralManager.start();
-    }
-
-    @After
-    public void tearDown() {
-        if (mCarBlePeripheralManager != null) {
-            mCarBlePeripheralManager.stop();
-        }
-        if (mMockitoSession != null) {
-            mMockitoSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void testStartAssociationAdvertisingSuccess() {
-        String testDeviceName = getNameForAssociation();
-        startAssociation(mAssociationCallback, testDeviceName);
-        ArgumentCaptor<AdvertiseData> advertisementDataCaptor =
-                ArgumentCaptor.forClass(AdvertiseData.class);
-        ArgumentCaptor<AdvertiseData> scanResponseDataCaptor =
-                ArgumentCaptor.forClass(AdvertiseData.class);
-        verify(mMockPeripheralManager).startAdvertising(any(), advertisementDataCaptor.capture(),
-                scanResponseDataCaptor.capture(), any());
-        AdvertiseData advertisementData = advertisementDataCaptor.getValue();
-        ParcelUuid serviceUuid = new ParcelUuid(ASSOCIATION_SERVICE_UUID);
-        assertThat(advertisementData.getServiceUuids()).contains(serviceUuid);
-        AdvertiseData scanResponseData = scanResponseDataCaptor.getValue();
-        assertThat(scanResponseData.getIncludeDeviceName()).isFalse();
-        ParcelUuid dataUuid = new ParcelUuid(RECONNECT_DATA_UUID);
-        assertThat(scanResponseData.getServiceData().get(dataUuid)).isEqualTo(
-                testDeviceName.getBytes());
-    }
-
-    @Test
-    public void testStartAssociationAdvertisingFailure() {
-        startAssociation(mAssociationCallback, getNameForAssociation());
-        ArgumentCaptor<AdvertiseCallback> callbackCaptor =
-                ArgumentCaptor.forClass(AdvertiseCallback.class);
-        verify(mMockPeripheralManager).startAdvertising(any(), any(), any(),
-                callbackCaptor.capture());
-        AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
-        int testErrorCode = 2;
-        advertiseCallback.onStartFailure(testErrorCode);
-        verify(mAssociationCallback).onAssociationStartFailure();
-    }
-
-    @Test
-    public void testNotifyAssociationSuccess() {
-        String testDeviceName = getNameForAssociation();
-        startAssociation(mAssociationCallback, testDeviceName);
-        ArgumentCaptor<AdvertiseCallback> callbackCaptor =
-                ArgumentCaptor.forClass(AdvertiseCallback.class);
-        verify(mMockPeripheralManager).startAdvertising(any(), any(), any(),
-                callbackCaptor.capture());
-        AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
-        AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
-        advertiseCallback.onStartSuccess(settings);
-        verify(mAssociationCallback).onAssociationStartSuccess(eq(testDeviceName));
-    }
-
-    @Test
-    public void testShowVerificationCode() {
-        AssociationSecureChannel channel = getChannelForAssociation(mAssociationCallback);
-        channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE);
-        verify(mAssociationCallback).onVerificationCodeAvailable(eq(TEST_VERIFICATION_CODE));
-    }
-
-    @Test
-    public void testAssociationSuccess() {
-        SecureChannel channel = getChannelForAssociation(mAssociationCallback);
-        SecureChannel.Callback channelCallback = channel.getCallback();
-        assertThat(channelCallback).isNotNull();
-        channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
-        channelCallback.onSecureChannelEstablished();
-        ArgumentCaptor<AssociatedDevice> deviceCaptor =
-                ArgumentCaptor.forClass(AssociatedDevice.class);
-        verify(mMockStorage).addAssociatedDeviceForActiveUser(deviceCaptor.capture());
-        AssociatedDevice device = deviceCaptor.getValue();
-        assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
-        verify(mAssociationCallback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString()));
-    }
-
-    @Test
-    public void testAssociationFailure_channelError() {
-        SecureChannel channel = getChannelForAssociation(mAssociationCallback);
-        SecureChannel.Callback channelCallback = channel.getCallback();
-        int testErrorCode = 1;
-        assertThat(channelCallback).isNotNull();
-        channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
-        channelCallback.onEstablishSecureChannelFailure(testErrorCode);
-        verify(mAssociationCallback).onAssociationError(eq(testErrorCode));
-    }
-
-    @Test
-    public void connectToDevice_stopsAdvertisingAfterTimeout() {
-        when(mMockStorage.hashWithChallengeSecret(any(), any()))
-                .thenReturn(ByteUtils.randomBytes(32));
-        mCarBlePeripheralManager.connectToDevice(UUID.randomUUID());
-        ArgumentCaptor<AdvertiseCallback> callbackCaptor =
-                ArgumentCaptor.forClass(AdvertiseCallback.class);
-        verify(mMockPeripheralManager).startAdvertising(any(), any(), any(),
-                callbackCaptor.capture());
-        callbackCaptor.getValue().onStartSuccess(null);
-        verify(mMockPeripheralManager,
-                timeout(RECONNECT_ADVERTISEMENT_DURATION.plusSeconds(1).toMillis()))
-                .stopAdvertising(any(AdvertiseCallback.class));
-    }
-
-    @Test
-    public void disconnectDevice_stopsAdvertisingForPendingReconnect() {
-        when(mMockStorage.hashWithChallengeSecret(any(), any()))
-                .thenReturn(ByteUtils.randomBytes(32));
-        UUID deviceId = UUID.randomUUID();
-        mCarBlePeripheralManager.connectToDevice(deviceId);
-        reset(mMockPeripheralManager);
-        mCarBlePeripheralManager.disconnectDevice(deviceId.toString());
-        verify(mMockPeripheralManager).cleanup();
-    }
-
-    private BlePeripheralManager.Callback startAssociation(AssociationCallback callback,
-            String deviceName) {
-        ArgumentCaptor<BlePeripheralManager.Callback> callbackCaptor =
-                ArgumentCaptor.forClass(BlePeripheralManager.Callback.class);
-        mCarBlePeripheralManager.startAssociation(deviceName, callback);
-        verify(mMockPeripheralManager, timeout(3000)).registerCallback(callbackCaptor.capture());
-        return callbackCaptor.getValue();
-    }
-
-    private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) {
-        BlePeripheralManager.Callback bleManagerCallback = startAssociation(callback,
-                getNameForAssociation());
-        BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter()
-                .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
-        bleManagerCallback.onRemoteDeviceConnected(bluetoothDevice);
-        return (AssociationSecureChannel) mCarBlePeripheralManager.getConnectedDeviceChannel();
-    }
-
-    private String getNameForAssociation() {
-        return ByteUtils.generateRandomNumberString(DEVICE_NAME_LENGTH_LIMIT);
-
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/CarSppManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/CarSppManagerTest.java
deleted file mode 100644
index ef0ee85..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/CarSppManagerTest.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-
-import androidx.annotation.NonNull;
-
-import com.android.car.connecteddevice.AssociationCallback;
-import com.android.car.connecteddevice.connection.AssociationSecureChannel;
-import com.android.car.connecteddevice.connection.SecureChannel;
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.util.UUID;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-public class CarSppManagerTest {
-    private static final String TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB";
-    private static final UUID TEST_REMOTE_DEVICE_ID = UUID.randomUUID();
-    private static final UUID TEST_SERVICE_UUID = UUID.randomUUID();
-    private static final String TEST_VERIFICATION_CODE = "000000";
-    private static final int MAX_PACKET_SIZE = 700;
-    @Mock
-    private SppManager mMockSppManager;
-    @Mock
-    private ConnectedDeviceStorage mMockStorage;
-
-    private CarSppManager mCarSppManager;
-
-    private MockitoSession mMockitoSession;
-
-    @Before
-    public void setUp() throws Exception {
-        mMockitoSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-        mCarSppManager = new CarSppManager(mMockSppManager, mMockStorage, TEST_SERVICE_UUID,
-                MAX_PACKET_SIZE);
-    }
-
-    @After
-    public void tearDown() {
-        if (mCarSppManager != null) {
-            mCarSppManager.stop();
-        }
-        if (mMockitoSession != null) {
-            mMockitoSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void testStartAssociationSuccess() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        AssociationCallback callback = createAssociationCallback(semaphore);
-        when(mMockSppManager.startListening(TEST_SERVICE_UUID)).thenReturn(true);
-
-        mCarSppManager.startAssociation(null, callback);
-
-        verify(mMockSppManager).startListening(TEST_SERVICE_UUID);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(callback).onAssociationStartSuccess(eq(null));
-    }
-
-    @Test
-    public void testStartAssociationFailure() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        AssociationCallback callback = createAssociationCallback(semaphore);
-        when(mMockSppManager.startListening(TEST_SERVICE_UUID)).thenReturn(false);
-
-        mCarSppManager.startAssociation(null, callback);
-
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(callback).onAssociationStartFailure();
-    }
-
-    @Test
-    public void testShowVerificationCode() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        AssociationCallback callback = createAssociationCallback(semaphore);
-        AssociationSecureChannel channel = getChannelForAssociation(callback);
-
-        channel.getShowVerificationCodeListener().showVerificationCode(TEST_VERIFICATION_CODE);
-
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(callback).onVerificationCodeAvailable(eq(TEST_VERIFICATION_CODE));
-    }
-
-    @Test
-    public void testAssociationSuccess() throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        AssociationCallback callback = createAssociationCallback(semaphore);
-        SecureChannel channel = getChannelForAssociation(callback);
-        SecureChannel.Callback channelCallback = channel.getCallback();
-
-        assertThat(channelCallback).isNotNull();
-
-        channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
-        channelCallback.onSecureChannelEstablished();
-        ArgumentCaptor<AssociatedDevice> deviceCaptor =
-                ArgumentCaptor.forClass(AssociatedDevice.class);
-        verify(mMockStorage).addAssociatedDeviceForActiveUser(deviceCaptor.capture());
-        AssociatedDevice device = deviceCaptor.getValue();
-
-        assertThat(device.getDeviceId()).isEqualTo(TEST_REMOTE_DEVICE_ID.toString());
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(callback).onAssociationCompleted(eq(TEST_REMOTE_DEVICE_ID.toString()));
-    }
-
-    private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
-        return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
-    }
-
-    private AssociationSecureChannel getChannelForAssociation(AssociationCallback callback) {
-        ArgumentCaptor<SppManager.ConnectionCallback> callbackCaptor =
-                ArgumentCaptor.forClass(SppManager.ConnectionCallback.class);
-        mCarSppManager.startAssociation(null, callback);
-        verify(mMockSppManager).registerCallback(callbackCaptor.capture(), any());
-        BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter()
-                .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
-        callbackCaptor.getValue().onRemoteDeviceConnected(bluetoothDevice);
-        return (AssociationSecureChannel) mCarSppManager.getConnectedDeviceChannel();
-    }
-
-    @NonNull
-    private AssociationCallback createAssociationCallback(@NonNull final Semaphore semaphore) {
-        return spy(new AssociationCallback() {
-            @Override
-            public void onAssociationStartSuccess(String deviceName) {
-                semaphore.release();
-            }
-
-            @Override
-            public void onAssociationStartFailure() {
-                semaphore.release();
-            }
-
-            @Override
-            public void onAssociationError(int error) {
-                semaphore.release();
-            }
-
-            @Override
-            public void onVerificationCodeAvailable(String code) {
-                semaphore.release();
-            }
-
-            @Override
-            public void onAssociationCompleted(String deviceId) {
-                semaphore.release();
-            }
-        });
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/ConnectedTaskTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/ConnectedTaskTest.java
deleted file mode 100644
index 7ae8192..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/ConnectedTaskTest.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class ConnectedTaskTest {
-
-    private final byte[] mTestData = "testData".getBytes();
-    private ConnectedTask mConnectedTask;
-    private InputStream mInputStream = new ByteArrayInputStream(mTestData);
-    private OutputStream mOutputStream = new ByteArrayOutputStream();
-    private ConnectedTask.Callback mCallback;
-    private Executor mExecutor = Executors.newSingleThreadExecutor();
-    private Semaphore mSemaphore = new Semaphore(0);
-
-
-    @Before
-    public void setUp() {
-        mCallback = spy(new ConnectedTask.Callback() {
-            @Override
-            public void onMessageReceived(byte[] message) {
-                mSemaphore.release();
-            }
-
-            @Override
-            public void onDisconnected() {
-
-            }
-        });
-        mConnectedTask = new ConnectedTask(mInputStream, mOutputStream, null, mCallback);
-    }
-
-    @Test
-    public void testTaskRun_InformCallback() throws InterruptedException {
-        mExecutor.execute(mConnectedTask);
-        assertThat(tryAcquire(mSemaphore)).isTrue();
-        verify(mCallback).onMessageReceived(mTestData);
-    }
-
-    @Test
-    public void testWrite_WriteToOutputStream() {
-        mConnectedTask.write(mTestData);
-        assertThat(mOutputStream.toString()).isEqualTo(new String(mTestData));
-    }
-
-    private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
-        return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java
deleted file mode 100644
index a126d04..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppDeviceMessageStreamTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-@RunWith(AndroidJUnit4.class)
-public class SppDeviceMessageStreamTest {
-    private static final int MAX_WRITE_SIZE = 700;
-
-    @Mock
-    private SppManager mMockSppManager;
-    private BluetoothDevice mBluetoothDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(
-            "00:11:22:33:44:55");
-    private MockitoSession mMockingSession;
-    private SppDeviceMessageStream mSppDeviceMessageStream;
-
-    @Before
-    public void setUp() {
-        mMockingSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-        mSppDeviceMessageStream = spy(
-                new SppDeviceMessageStream(mMockSppManager, mBluetoothDevice, MAX_WRITE_SIZE));
-    }
-
-    @After
-    public void tearDown() {
-        if (mMockingSession != null) {
-            mMockingSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void send_callsWriteAndSendCompleted() {
-        byte[] data = ByteUtils.randomBytes(10);
-        mSppDeviceMessageStream.send(data);
-        verify(mMockSppManager).write(data);
-        verify(mSppDeviceMessageStream).sendCompleted();
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppManagerTest.java
deleted file mode 100644
index a99fc6b..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppManagerTest.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-
-import android.bluetooth.BluetoothDevice;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.io.IOException;
-import java.util.UUID;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(AndroidJUnit4.class)
-public class SppManagerTest {
-    private static final UUID TEST_SERVICE_UUID = UUID.randomUUID();
-    private final boolean mIsSecureRfcommChannel = true;
-    private final byte[] mTestData = "testData".getBytes();
-    private SppManager mSppManager;
-    private Executor mCallbackExecutor = Executors.newSingleThreadExecutor();
-    private byte[] mCompletedMessage = SppPayloadStream.wrapWithArrayLength(mTestData);
-    @Mock
-    private ConnectedTask mMockConnectedTask;
-
-    @Mock
-    private ExecutorService mMockExecutorService;
-    private MockitoSession mMockitoSession;
-
-
-    @Before
-    public void setUp() throws IOException {
-        mSppManager = new SppManager(mIsSecureRfcommChannel);
-        mMockitoSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-    }
-
-    @After
-    public void tearDown() {
-        if (mMockitoSession != null) {
-            mMockitoSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void testStartListen_StartAcceptTask() {
-        mSppManager.mConnectionExecutor = mMockExecutorService;
-        mSppManager.startListening(TEST_SERVICE_UUID);
-        assertThat(mSppManager.mAcceptTask).isNotNull();
-        verify(mMockExecutorService).execute(mSppManager.mAcceptTask);
-    }
-
-    @Test
-    public void testWrite_CallConnectedTaskToWrite() {
-        mSppManager.mConnectedTask = mMockConnectedTask;
-        mSppManager.mState = SppManager.ConnectionState.CONNECTED;
-        mSppManager.write(mTestData);
-        verify(mMockConnectedTask).write(SppPayloadStream.wrapWithArrayLength(mTestData));
-    }
-
-    @Test
-    public void testConnectedTaskCallback_onMessageReceived_CallOnMessageReceivedListener()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        SppManager.OnMessageReceivedListener listener = createOnMessageReceivedListener(semaphore);
-        mSppManager.addOnMessageReceivedListener(listener, mCallbackExecutor);
-        mSppManager.mConnectedTaskCallback.onMessageReceived(mCompletedMessage);
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(listener).onMessageReceived(any(), eq(mTestData));
-    }
-
-    @Test
-    public void testConnectedTaskCallback_onDisconnected_CallOnRemoteDeviceDisconnected()
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-        SppManager.ConnectionCallback callback = createConnectionCallback(semaphore);
-        mSppManager.registerCallback(callback, mCallbackExecutor);
-        mSppManager.mConnectedTaskCallback.onDisconnected();
-        assertThat(tryAcquire(semaphore)).isTrue();
-        verify(callback).onRemoteDeviceDisconnected(any());
-    }
-
-    @NonNull
-    private SppManager.ConnectionCallback createConnectionCallback(
-            @NonNull final Semaphore semaphore) {
-        return spy(new SppManager.ConnectionCallback() {
-
-            @Override
-            public void onRemoteDeviceConnected(BluetoothDevice device) {
-                semaphore.release();
-            }
-
-            @Override
-            public void onRemoteDeviceDisconnected(BluetoothDevice device) {
-                semaphore.release();
-            }
-        });
-    }
-
-    @NonNull
-    private SppManager.OnMessageReceivedListener createOnMessageReceivedListener(
-            @NonNull final Semaphore semaphore) {
-        return spy((device, value) -> semaphore.release());
-    }
-
-    private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
-        return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppPayloadStreamTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppPayloadStreamTest.java
deleted file mode 100644
index 9f0b42b..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/connection/spp/SppPayloadStreamTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.connection.spp;
-
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-@RunWith(AndroidJUnit4.class)
-public class SppPayloadStreamTest {
-    private SppPayloadStream mSppPayloadStream;
-    private MockitoSession mMockitoSession;
-    private final byte[] mTestData = "testData".getBytes();
-    private byte[] mCompletedMessage = SppPayloadStream.wrapWithArrayLength(mTestData);
-    ;
-    private byte[] mCompletedMessageSplit1;
-    private byte[] mCompletedMessageSplit2;
-    @Mock
-    private SppPayloadStream.OnMessageCompletedListener mMockListener;
-
-
-    @Before
-    public void setUp() {
-        mMockitoSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-        mSppPayloadStream = new SppPayloadStream();
-        mSppPayloadStream.setMessageCompletedListener(mMockListener);
-        int length = mCompletedMessage.length;
-        mCompletedMessageSplit1 = Arrays.copyOfRange(mCompletedMessage, 0, (length + 1) / 2);
-        mCompletedMessageSplit2 = Arrays.copyOfRange(mCompletedMessage, (length + 1) / 2, length);
-    }
-
-    @After
-    public void tearDown() {
-        if (mMockitoSession != null) {
-            mMockitoSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void testWriteCompletedMessage_InformListener() throws IOException {
-        mSppPayloadStream.write(mCompletedMessage);
-        verify(mMockListener).onMessageCompleted(mTestData);
-    }
-
-    @Test
-    public void testWriteIncompleteMessage_DoNotInformListener() throws IOException {
-        mSppPayloadStream.write(mCompletedMessageSplit1);
-        verify(mMockListener, never()).onMessageCompleted(mTestData);
-    }
-
-    @Test
-    public void testWriteTwoMessage_InformListenerCompletedMessage() throws IOException {
-        mSppPayloadStream.write(mCompletedMessageSplit1);
-        mSppPayloadStream.write(mCompletedMessageSplit2);
-
-        verify(mMockListener).onMessageCompleted(mTestData);
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannelTest.java
deleted file mode 100644
index 9628afa..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/BluetoothRfcommChannelTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.oob;
-
-
-import static com.android.car.connecteddevice.model.OobEligibleDevice.OOB_TYPE_BLUETOOTH;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mockitoSession;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothSocket;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.UUID;
-
-@RunWith(AndroidJUnit4.class)
-public class BluetoothRfcommChannelTest {
-    private BluetoothRfcommChannel mBluetoothRfcommChannel;
-    private OobEligibleDevice mOobEligibleDevice;
-    @Mock
-    private OobChannel.Callback mMockCallback;
-    @Mock
-    private BluetoothAdapter mMockBluetoothAdapter;
-    @Mock
-    private BluetoothDevice mMockBluetoothDevice;
-    @Mock
-    private BluetoothSocket mMockBluetoothSocket;
-    @Mock
-    private OutputStream mMockOutputStream;
-
-    private MockitoSession mMockingSession;
-
-    @Before
-    public void setUp() {
-        mMockingSession = mockitoSession()
-                .initMocks(this)
-                .strictness(Strictness.WARN)
-                .startMocking();
-
-
-        mBluetoothRfcommChannel = new BluetoothRfcommChannel();
-
-        mOobEligibleDevice = new OobEligibleDevice("00:11:22:33:44:55", OOB_TYPE_BLUETOOTH);
-
-        when(mMockBluetoothAdapter.getRemoteDevice(
-                mOobEligibleDevice.getDeviceAddress())).thenReturn(mMockBluetoothDevice);
-    }
-
-    @After
-    public void tearDown() {
-        if (mMockingSession != null) {
-            mMockingSession.finishMocking();
-        }
-    }
-
-    @Test
-    public void completeOobExchange_success() throws Exception {
-        when(mMockBluetoothSocket.getOutputStream()).thenReturn(mMockOutputStream);
-        when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenReturn(
-                mMockBluetoothSocket);
-        mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
-                mMockBluetoothAdapter);
-
-        verify(mMockCallback, timeout(1000)).onOobExchangeSuccess();
-        OobConnectionManager oobConnectionManager = new OobConnectionManager();
-        oobConnectionManager.startOobExchange(mBluetoothRfcommChannel);
-
-        ArgumentCaptor<byte[]> oobDataCaptor = ArgumentCaptor.forClass(byte[].class);
-        verify(mMockOutputStream).write(oobDataCaptor.capture());
-        byte[] oobData = oobDataCaptor.getValue();
-
-        assertThat(oobData).isEqualTo(Bytes.concat(oobConnectionManager.mDecryptionIv,
-                oobConnectionManager.mEncryptionIv,
-                oobConnectionManager.mEncryptionKey.getEncoded()));
-    }
-
-    @Test
-    public void completeOobExchange_ioExceptionCausesRetry() throws Exception {
-        doThrow(IOException.class).doAnswer(invocation -> null)
-                .when(mMockBluetoothSocket).connect();
-        when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenReturn(
-                mMockBluetoothSocket);
-        mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
-                mMockBluetoothAdapter);
-        verify(mMockBluetoothSocket, timeout(3000).times(2)).connect();
-    }
-
-    @Test
-    public void completeOobExchange_createRfcommSocketFails_callOnFailed() throws Exception {
-        when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenThrow(
-                IOException.class);
-
-        mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
-                mMockBluetoothAdapter);
-        verify(mMockCallback).onOobExchangeFailure();
-    }
-
-    @Test
-    public void sendOobData_nullBluetoothDevice_callOnFailed() {
-        mBluetoothRfcommChannel.mCallback = mMockCallback;
-
-        mBluetoothRfcommChannel.sendOobData("someData".getBytes());
-        verify(mMockCallback).onOobExchangeFailure();
-    }
-
-    @Test
-    public void sendOobData_writeFails_callOnFailed() throws Exception {
-        byte[] testMessage = "testMessage".getBytes();
-        completeOobExchange_success();
-        doThrow(IOException.class).when(mMockOutputStream).write(testMessage);
-
-        mBluetoothRfcommChannel.sendOobData(testMessage);
-        verify(mMockCallback).onOobExchangeFailure();
-    }
-
-    @Test
-    public void interrupt_closesSocket() throws Exception {
-        when(mMockBluetoothSocket.getOutputStream()).thenReturn(mMockOutputStream);
-        when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenReturn(
-                mMockBluetoothSocket);
-        mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
-                mMockBluetoothAdapter);
-        mBluetoothRfcommChannel.interrupt();
-        mBluetoothRfcommChannel.sendOobData(ByteUtils.randomBytes(10));
-        verify(mMockOutputStream, times(0)).write(any());
-        verify(mMockOutputStream).flush();
-        verify(mMockOutputStream).close();
-    }
-
-    @Test
-    public void interrupt_preventsCallbacks() throws Exception {
-        when(mMockBluetoothSocket.getOutputStream()).thenReturn(mMockOutputStream);
-        when(mMockBluetoothDevice.createRfcommSocketToServiceRecord(any(UUID.class))).thenReturn(
-                mMockBluetoothSocket);
-        doAnswer(invocation -> {
-            mBluetoothRfcommChannel.interrupt();
-            return invocation.callRealMethod();
-        }).when(mMockBluetoothSocket).connect();
-        mBluetoothRfcommChannel.completeOobDataExchange(mOobEligibleDevice, mMockCallback,
-                mMockBluetoothAdapter);
-        verify(mMockCallback, times(0)).onOobExchangeSuccess();
-        verify(mMockCallback, times(0)).onOobExchangeFailure();
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/OobConnectionManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/OobConnectionManagerTest.java
deleted file mode 100644
index 13f93c9..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/oob/OobConnectionManagerTest.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.oob;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.security.keystore.KeyProperties;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.model.OobEligibleDevice;
-
-import com.google.common.primitives.Bytes;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.InvalidKeyException;
-import java.security.SecureRandom;
-
-import javax.crypto.AEADBadTagException;
-import javax.crypto.KeyGenerator;
-import javax.crypto.SecretKey;
-
-@RunWith(AndroidJUnit4.class)
-public class OobConnectionManagerTest {
-    private static final byte[] TEST_MESSAGE = "testMessage".getBytes();
-    private TestChannel mTestChannel;
-    private SecretKey mTestKey;
-    private byte[] mTestEncryptionIv = new byte[OobConnectionManager.NONCE_LENGTH_BYTES];
-    private byte[] mTestDecryptionIv = new byte[OobConnectionManager.NONCE_LENGTH_BYTES];
-    private byte[] mTestOobData;
-
-    @Before
-    public void setUp() throws Exception {
-        mTestChannel = new TestChannel();
-        mTestKey = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES).generateKey();
-
-        SecureRandom secureRandom = new SecureRandom();
-        secureRandom.nextBytes(mTestEncryptionIv);
-        secureRandom.nextBytes(mTestDecryptionIv);
-
-        mTestOobData = Bytes.concat(mTestDecryptionIv, mTestEncryptionIv, mTestKey.getEncoded());
-    }
-
-    @Test
-    public void testInitAsServer_keyIsNull() {
-        OobConnectionManager oobConnectionManager = new OobConnectionManager();
-        assertThat(oobConnectionManager.mEncryptionKey).isNull();
-    }
-
-    @Test
-    public void testServer_onSetOobData_setsKeyAndNonce() {
-        OobConnectionManager oobConnectionManager = new OobConnectionManager();
-        oobConnectionManager.setOobData(mTestOobData);
-        assertThat(oobConnectionManager.mEncryptionKey).isEqualTo(mTestKey);
-        // The decryption IV for the server is the encryption IV for the client and vice versa
-        assertThat(oobConnectionManager.mDecryptionIv).isEqualTo(mTestEncryptionIv);
-        assertThat(oobConnectionManager.mEncryptionIv).isEqualTo(mTestDecryptionIv);
-    }
-
-    @Test
-    public void testInitAsClient_keyAndNoncesAreNonNullAndSent() {
-        OobConnectionManager oobConnectionManager = new OobConnectionManager();
-        oobConnectionManager.startOobExchange(mTestChannel);
-        assertThat(oobConnectionManager.mEncryptionKey).isNotNull();
-        assertThat(oobConnectionManager.mEncryptionIv).isNotNull();
-        assertThat(oobConnectionManager.mDecryptionIv).isNotNull();
-        assertThat(mTestChannel.mSentOobData).isEqualTo(Bytes.concat(
-                oobConnectionManager.mDecryptionIv,
-                oobConnectionManager.mEncryptionIv,
-                oobConnectionManager.mEncryptionKey.getEncoded()
-        ));
-    }
-
-    @Test
-    public void testServerEncryptAndClientDecrypt() throws Exception {
-        OobConnectionManager clientOobConnectionManager = new OobConnectionManager();
-        clientOobConnectionManager.startOobExchange(mTestChannel);
-        OobConnectionManager serverOobConnectionManager = new OobConnectionManager();
-        serverOobConnectionManager.setOobData(mTestChannel.mSentOobData);
-
-        byte[] encryptedTestMessage = clientOobConnectionManager.encryptVerificationCode(
-                TEST_MESSAGE);
-        byte[] decryptedTestMessage = serverOobConnectionManager.decryptVerificationCode(
-                encryptedTestMessage);
-
-        assertThat(decryptedTestMessage).isEqualTo(TEST_MESSAGE);
-    }
-
-    @Test
-    public void testClientEncryptAndServerDecrypt() throws Exception {
-        OobConnectionManager clientOobConnectionManager = new OobConnectionManager();
-        clientOobConnectionManager.startOobExchange(mTestChannel);
-        OobConnectionManager serverOobConnectionManager = new OobConnectionManager();
-        serverOobConnectionManager.setOobData(mTestChannel.mSentOobData);
-
-        byte[] encryptedTestMessage = serverOobConnectionManager.encryptVerificationCode(
-                TEST_MESSAGE);
-        byte[] decryptedTestMessage = clientOobConnectionManager.decryptVerificationCode(
-                encryptedTestMessage);
-
-        assertThat(decryptedTestMessage).isEqualTo(TEST_MESSAGE);
-    }
-
-    @Test
-    public void testEncryptAndDecryptWithDifferentNonces_throwsAEADBadTagException()
-            throws Exception {
-        // The OobConnectionManager stores a different nonce for encryption and decryption, so it
-        // can't decrypt messages that it encrypted itself. It can only send encrypted messages to
-        // an OobConnectionManager on another device that share its nonces and encryption key.
-        OobConnectionManager oobConnectionManager = new OobConnectionManager();
-        oobConnectionManager.startOobExchange(mTestChannel);
-        byte[] encryptedMessage = oobConnectionManager.encryptVerificationCode(TEST_MESSAGE);
-        assertThrows(AEADBadTagException.class,
-                () -> oobConnectionManager.decryptVerificationCode(encryptedMessage));
-    }
-
-    @Test
-    public void testDecryptWithShortMessage_throwsAEADBadTagException() {
-        OobConnectionManager oobConnectionManager = new OobConnectionManager();
-        oobConnectionManager.startOobExchange(mTestChannel);
-        assertThrows(AEADBadTagException.class,
-                () -> oobConnectionManager.decryptVerificationCode("short".getBytes()));
-    }
-
-    @Test
-    public void testEncryptWithNullKey_throwsInvalidKeyException() {
-        OobConnectionManager oobConnectionManager = new OobConnectionManager();
-        assertThrows(InvalidKeyException.class,
-                () -> oobConnectionManager.encryptVerificationCode(TEST_MESSAGE));
-    }
-
-    @Test
-    public void testDecryptWithNullKey_throwsInvalidKeyException() {
-        OobConnectionManager oobConnectionManager = new OobConnectionManager();
-        assertThrows(InvalidKeyException.class,
-                () -> oobConnectionManager.decryptVerificationCode(TEST_MESSAGE));
-    }
-
-    private static class TestChannel implements OobChannel {
-        byte[] mSentOobData = null;
-
-        @Override
-        public void completeOobDataExchange(OobEligibleDevice device, Callback callback) {
-
-        }
-
-        @Override
-        public void sendOobData(byte[] oobData) {
-            mSentOobData = oobData;
-        }
-
-        @Override
-        public void interrupt() {
-
-        }
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorageTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorageTest.java
deleted file mode 100644
index 755ce64..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorageTest.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.storage;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.testng.Assert.assertThrows;
-
-import android.content.Context;
-import android.util.Pair;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.car.connecteddevice.model.AssociatedDevice;
-import com.android.car.connecteddevice.util.ByteUtils;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.security.InvalidParameterException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
-@RunWith(AndroidJUnit4.class)
-public final class ConnectedDeviceStorageTest {
-    private final Context mContext = ApplicationProvider.getApplicationContext();
-
-    private final int mActiveUserId = 10;
-
-    private ConnectedDeviceStorage mConnectedDeviceStorage;
-
-    private List<Pair<Integer, AssociatedDevice>> mAddedAssociatedDevices;
-
-    @Before
-    public void setUp() {
-        mConnectedDeviceStorage = new ConnectedDeviceStorage(mContext);
-        mAddedAssociatedDevices = new ArrayList<>();
-    }
-
-    @After
-    public void tearDown() {
-        // Clear any associated devices added during tests.
-        for (Pair<Integer, AssociatedDevice> device : mAddedAssociatedDevices) {
-            mConnectedDeviceStorage.removeAssociatedDevice(device.first,
-                    device.second.getDeviceId());
-        }
-    }
-
-    @Test
-    public void getAssociatedDeviceIdsForUser_includesNewlyAddedDevice() {
-        AssociatedDevice addedDevice = addRandomAssociatedDevice(mActiveUserId);
-        List<String> associatedDevices =
-                mConnectedDeviceStorage.getAssociatedDeviceIdsForUser(mActiveUserId);
-        assertThat(associatedDevices).containsExactly(addedDevice.getDeviceId());
-    }
-
-    @Test
-    public void getAssociatedDeviceIdsForUser_excludesDeviceAddedForOtherUser() {
-        addRandomAssociatedDevice(mActiveUserId);
-        List<String> associatedDevices =
-                mConnectedDeviceStorage.getAssociatedDeviceIdsForUser(mActiveUserId + 1);
-        assertThat(associatedDevices).isEmpty();
-    }
-
-    @Test
-    public void getAssociatedDeviceIdsForUser_excludesRemovedDevice() {
-        AssociatedDevice addedDevice = addRandomAssociatedDevice(mActiveUserId);
-        mConnectedDeviceStorage.removeAssociatedDevice(mActiveUserId, addedDevice.getDeviceId());
-        List<String> associatedDevices =
-                mConnectedDeviceStorage.getAssociatedDeviceIdsForUser(mActiveUserId);
-        assertThat(associatedDevices).isEmpty();
-    }
-
-    @Test
-    public void getAssociatedDevicesForUser_includesNewlyAddedDevice() {
-        AssociatedDevice addedDevice = addRandomAssociatedDevice(mActiveUserId);
-        List<AssociatedDevice> associatedDevices =
-                mConnectedDeviceStorage.getAssociatedDevicesForUser(mActiveUserId);
-        assertThat(associatedDevices).containsExactly(addedDevice);
-    }
-
-    @Test
-    public void getAssociatedDevicesForUser_excludesDeviceAddedForOtherUser() {
-        addRandomAssociatedDevice(mActiveUserId);
-        List<String> associatedDevices =
-                mConnectedDeviceStorage.getAssociatedDeviceIdsForUser(mActiveUserId + 1);
-        assertThat(associatedDevices).isEmpty();
-    }
-
-    @Test
-    public void getAssociatedDevicesForUser_excludesRemovedDevice() {
-        AssociatedDevice addedDevice = addRandomAssociatedDevice(mActiveUserId);
-        mConnectedDeviceStorage.removeAssociatedDevice(mActiveUserId, addedDevice.getDeviceId());
-        List<AssociatedDevice> associatedDevices =
-                mConnectedDeviceStorage.getAssociatedDevicesForUser(mActiveUserId);
-        assertThat(associatedDevices).isEmpty();
-    }
-
-    @Test
-    public void getEncryptionKey_returnsSavedKey() {
-        String deviceId = addRandomAssociatedDevice(mActiveUserId).getDeviceId();
-        byte[] key = ByteUtils.randomBytes(16);
-        mConnectedDeviceStorage.saveEncryptionKey(deviceId, key);
-        assertThat(mConnectedDeviceStorage.getEncryptionKey(deviceId)).isEqualTo(key);
-    }
-
-    @Test
-    public void getEncryptionKey_returnsNullForUnrecognizedDeviceId() {
-        String deviceId = addRandomAssociatedDevice(mActiveUserId).getDeviceId();
-        mConnectedDeviceStorage.saveEncryptionKey(deviceId, ByteUtils.randomBytes(16));
-        assertThat(mConnectedDeviceStorage.getEncryptionKey(UUID.randomUUID().toString())).isNull();
-    }
-
-    @Test
-    public void saveChallengeSecret_throwsForInvalidLengthSecret() {
-        byte[] invalidSecret =
-                ByteUtils.randomBytes(ConnectedDeviceStorage.CHALLENGE_SECRET_BYTES - 1);
-        assertThrows(InvalidParameterException.class,
-                () -> mConnectedDeviceStorage.saveChallengeSecret(UUID.randomUUID().toString(),
-                        invalidSecret));
-    }
-
-    private AssociatedDevice addRandomAssociatedDevice(int userId) {
-        AssociatedDevice device = new AssociatedDevice(UUID.randomUUID().toString(),
-                "00:00:00:00:00:00", "Test Device", true);
-        addAssociatedDevice(userId, device, ByteUtils.randomBytes(16));
-        return device;
-    }
-
-    private void addAssociatedDevice(int userId, AssociatedDevice device, byte[] encryptionKey) {
-        mConnectedDeviceStorage.addAssociatedDeviceForUser(userId, device);
-        mConnectedDeviceStorage.saveEncryptionKey(device.getDeviceId(), encryptionKey);
-        mAddedAssociatedDevices.add(new Pair<>(userId, device));
-    }
-}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/util/ScanDataAnalyzerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/util/ScanDataAnalyzerTest.java
deleted file mode 100644
index 92e8d34..0000000
--- a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/util/ScanDataAnalyzerTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.connecteddevice.util;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.math.BigInteger;
-
-@RunWith(AndroidJUnit4.class)
-public class ScanDataAnalyzerTest {
-    private static final BigInteger CORRECT_DATA =
-            new BigInteger(
-                    "02011A14FF4C000100000000000000000000000000200000000000000000000000000000"
-                            + "0000000000000000000000000000000000000000000000000000",
-                    16);
-
-    private static final BigInteger CORRECT_MASK =
-            new BigInteger("00000000000000000000000000200000", 16);
-
-    private static final BigInteger MULTIPLE_BIT_MASK =
-            new BigInteger("00000000000000000100000000200000", 16);
-
-    @Test
-    public void containsUuidsInOverflow_correctBitFlipped_shouldReturnTrue() {
-        assertThat(
-                ScanDataAnalyzer.containsUuidsInOverflow(CORRECT_DATA.toByteArray(), CORRECT_MASK))
-                .isTrue();
-    }
-
-    @Test
-    public void containsUuidsInOverflow_bitNotFlipped_shouldReturnFalse() {
-        assertThat(
-                ScanDataAnalyzer.containsUuidsInOverflow(
-                        CORRECT_DATA.negate().toByteArray(), CORRECT_MASK))
-                .isFalse();
-    }
-
-    @Test
-    public void containsUuidsInOverflow_maskWithMultipleBitsIncompleteMatch_shouldReturnTrue() {
-        assertThat(
-                ScanDataAnalyzer.containsUuidsInOverflow(CORRECT_DATA.toByteArray(),
-                        MULTIPLE_BIT_MASK))
-                .isTrue();
-    }
-
-    @Test
-    public void containsUuidsInOverflow_incorrectLengthByte_shouldReturnFalse() {
-        // Incorrect length of 0x20
-        byte[] data =
-                new BigInteger(
-                        "02011A20FF4C00010000000000000000000000000020000000000000000000000000000000"
-                                + "00000000000000000000000000000000000000000000000000",
-                        16)
-                        .toByteArray();
-        BigInteger mask = new BigInteger("00000000000000000000000000200000", 16);
-        assertThat(ScanDataAnalyzer.containsUuidsInOverflow(data, mask)).isFalse();
-    }
-
-    @Test
-    public void containsUuidsInOverflow_incorrectAdTypeByte_shouldReturnFalse() {
-        // Incorrect advertising type of 0xEF
-        byte[] data =
-                new BigInteger(
-                        "02011A14EF4C00010000000000000000000000000020000000000000000000000000000000"
-                                + "00000000000000000000000000000000000000000000000000",
-                        16)
-                        .toByteArray();
-        assertThat(ScanDataAnalyzer.containsUuidsInOverflow(data, CORRECT_MASK)).isFalse();
-    }
-
-    @Test
-    public void containsUuidsInOverflow_incorrectCustomId_shouldReturnFalse() {
-        // Incorrect custom id of 0x4C1001
-        byte[] data =
-                new BigInteger(
-                        "02011A14FF4C10010000000000000000000000000020000000000000000000000000000000"
-                                + "00000000000000000000000000000000000000000000000000",
-                        16)
-                        .toByteArray();
-        assertThat(ScanDataAnalyzer.containsUuidsInOverflow(data, CORRECT_MASK)).isFalse();
-    }
-
-    @Test
-    public void containsUuidsInOverflow_incorrectContentLength_shouldReturnFalse() {
-        byte[] data = new BigInteger("02011A14FF4C1001000000000000000000000000002", 16)
-                .toByteArray();
-        assertThat(ScanDataAnalyzer.containsUuidsInOverflow(data, CORRECT_MASK)).isFalse();
-    }
-}