Merge branch android10-qpr3-release

Change-Id: I7e968412018feda5d55de69a85fdc660355801f7
diff --git a/EncryptionRunner/Android.bp b/EncryptionRunner/Android.bp
new file mode 100644
index 0000000..54316ff
--- /dev/null
+++ b/EncryptionRunner/Android.bp
@@ -0,0 +1,31 @@
+// 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: "EncryptionRunner-lib",
+    min_sdk_version: "23",
+    product_variables: {
+        pdk: {
+            enabled: false,
+        },
+    },
+    static_libs: [
+      "ukey2",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+    installable: true,
+}
+
diff --git a/EncryptionRunner/AndroidManifest.xml b/EncryptionRunner/AndroidManifest.xml
new file mode 100644
index 0000000..3ebdf42
--- /dev/null
+++ b/EncryptionRunner/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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"
+        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+        package="android.car.encryptionrunner" >
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
+</manifest>
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/DummyEncryptionRunner.java b/EncryptionRunner/src/android/car/encryptionrunner/DummyEncryptionRunner.java
new file mode 100644
index 0000000..5b63dbc
--- /dev/null
+++ b/EncryptionRunner/src/android/car/encryptionrunner/DummyEncryptionRunner.java
@@ -0,0 +1,199 @@
+/*
+ * 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 android.car.encryptionrunner;
+
+import android.annotation.IntDef;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An encryption runner that doesn't actually do encryption. Useful for debugging. Do not use in
+ * production environments.
+ */
+@VisibleForTesting
+public class DummyEncryptionRunner implements EncryptionRunner {
+
+    private static final String KEY = "key";
+    private static final byte[] DUMMY_MESSAGE = "Dummy Message".getBytes();
+    @VisibleForTesting
+    public static final String INIT = "init";
+    @VisibleForTesting
+    public static final String INIT_RESPONSE = "initResponse";
+    @VisibleForTesting
+    public static final String CLIENT_RESPONSE = "clientResponse";
+    public static final String VERIFICATION_CODE = "1234";
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({Mode.UNKNOWN, Mode.CLIENT, Mode.SERVER})
+    private @interface Mode {
+
+        int UNKNOWN = 0;
+        int CLIENT = 1;
+        int SERVER = 2;
+    }
+
+    private boolean mIsReconnect;
+    private boolean mInitReconnectVerification;
+    private Key mCurrentDummyKey;
+    @Mode
+    private int mMode;
+    @HandshakeMessage.HandshakeState
+    private int mState;
+
+    @Override
+    public HandshakeMessage initHandshake() {
+        checkRunnerIsNew();
+        mMode = Mode.CLIENT;
+        mState = HandshakeMessage.HandshakeState.IN_PROGRESS;
+        return HandshakeMessage.newBuilder()
+                .setHandshakeState(mState)
+                .setNextMessage(INIT.getBytes())
+                .build();
+    }
+
+    @Override
+    public HandshakeMessage respondToInitRequest(byte[] initializationRequest)
+            throws HandshakeException {
+        checkRunnerIsNew();
+        mMode = Mode.SERVER;
+        if (!new String(initializationRequest).equals(INIT)) {
+            throw new HandshakeException("Unexpected initialization request");
+        }
+        mState = HandshakeMessage.HandshakeState.IN_PROGRESS;
+        return HandshakeMessage.newBuilder()
+                .setHandshakeState(HandshakeMessage.HandshakeState.IN_PROGRESS)
+                .setNextMessage(INIT_RESPONSE.getBytes())
+                .build();
+    }
+
+    private void checkRunnerIsNew() {
+        if (mState != HandshakeMessage.HandshakeState.UNKNOWN) {
+            throw new IllegalStateException("runner already initialized.");
+        }
+    }
+
+    @Override
+    public HandshakeMessage continueHandshake(byte[] response) throws HandshakeException {
+        if (mState != HandshakeMessage.HandshakeState.IN_PROGRESS) {
+            throw new HandshakeException("not waiting for response but got one");
+        }
+        switch (mMode) {
+            case Mode.SERVER:
+                if (!CLIENT_RESPONSE.equals(new String(response))) {
+                    throw new HandshakeException("unexpected response: " + new String(response));
+                }
+                mState = HandshakeMessage.HandshakeState.VERIFICATION_NEEDED;
+                if (mIsReconnect) {
+                    verifyPin();
+                    mState = HandshakeMessage.HandshakeState.RESUMING_SESSION;
+                }
+                return HandshakeMessage.newBuilder()
+                        .setVerificationCode(VERIFICATION_CODE)
+                        .setHandshakeState(mState)
+                        .build();
+            case Mode.CLIENT:
+                if (!INIT_RESPONSE.equals(new String(response))) {
+                    throw new HandshakeException("unexpected response: " + new String(response));
+                }
+                mState = HandshakeMessage.HandshakeState.VERIFICATION_NEEDED;
+                if (mIsReconnect) {
+                    verifyPin();
+                    mState = HandshakeMessage.HandshakeState.RESUMING_SESSION;
+                }
+                return HandshakeMessage.newBuilder()
+                        .setHandshakeState(mState)
+                        .setNextMessage(CLIENT_RESPONSE.getBytes())
+                        .setVerificationCode(VERIFICATION_CODE)
+                        .build();
+            default:
+                throw new IllegalStateException("unexpected role: " + mMode);
+        }
+    }
+
+    @Override
+    public HandshakeMessage authenticateReconnection(byte[] message, byte[] previousKey)
+            throws HandshakeException {
+        mCurrentDummyKey = new DummyKey();
+        // Blindly verify the reconnection because this is a dummy encryption runner.
+        return HandshakeMessage.newBuilder()
+                .setHandshakeState(HandshakeMessage.HandshakeState.FINISHED)
+                .setKey(mCurrentDummyKey)
+                .setNextMessage(mInitReconnectVerification ? null : DUMMY_MESSAGE)
+                .build();
+    }
+
+    @Override
+    public HandshakeMessage initReconnectAuthentication(byte[] previousKey)
+            throws HandshakeException {
+        mInitReconnectVerification = true;
+        mState = HandshakeMessage.HandshakeState.RESUMING_SESSION;
+        return HandshakeMessage.newBuilder()
+                .setHandshakeState(mState)
+                .setNextMessage(DUMMY_MESSAGE)
+                .build();
+    }
+
+    @Override
+    public Key keyOf(byte[] serialized) {
+        return new DummyKey();
+    }
+
+    @Override
+    public HandshakeMessage verifyPin() throws HandshakeException {
+        if (mState != HandshakeMessage.HandshakeState.VERIFICATION_NEEDED) {
+            throw new IllegalStateException("asking to verify pin, state = " + mState);
+        }
+        mState = HandshakeMessage.HandshakeState.FINISHED;
+        return HandshakeMessage.newBuilder().setKey(new DummyKey()).setHandshakeState(
+                mState).build();
+    }
+
+    @Override
+    public void invalidPin() {
+        mState = HandshakeMessage.HandshakeState.INVALID;
+    }
+
+    @Override
+    public void setIsReconnect(boolean isReconnect) {
+        mIsReconnect = isReconnect;
+    }
+
+    private class DummyKey implements Key {
+        @Override
+        public byte[] asBytes() {
+            return KEY.getBytes();
+        }
+
+        @Override
+        public byte[] encryptData(byte[] data) {
+            return data;
+        }
+
+        @Override
+        public byte[] decryptData(byte[] encryptedData) {
+            return encryptedData;
+        }
+
+        @Override
+        public byte[] getUniqueSession() {
+            return KEY.getBytes();
+        }
+    }
+}
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunner.java b/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunner.java
new file mode 100644
index 0000000..f0a34b2
--- /dev/null
+++ b/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunner.java
@@ -0,0 +1,181 @@
+/*
+ * 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 android.car.encryptionrunner;
+
+import android.annotation.NonNull;
+
+/**
+ * A generalized interface that allows for generating shared secrets as well as encrypting
+ * messages.
+ *
+ * To use this interface:
+ *
+ * <p>1. As a client.
+ *
+ * {@code
+ * HandshakeMessage initialClientMessage = clientRunner.initHandshake();
+ * sendToServer(initialClientMessage.getNextMessage());
+ * byte message = getServerResponse();
+ * HandshakeMessage message = clientRunner.continueHandshake(message);
+ * }
+ *
+ * <p>If it is a first-time connection,
+ *
+ * {@code message.getHandshakeState()} should be VERIFICATION_NEEDED, show user the verification
+ * code and ask to verify.
+ * After user confirmed, {@code HandshakeMessage lastMessage = clientRunner.verifyPin();} otherwise
+ * {@code clientRunner.invalidPin(); }
+ *
+ * Use {@code lastMessage.getKey()} to get the key for encryption.
+ *
+ * <p>If it is a reconnection,
+ *
+ * {@code message.getHandshakeState()} should be RESUMING_SESSION, PIN has been verified blindly,
+ * send the authentication message over to server, then authenticate the message from server.
+ *
+ * {@code
+ * clientMessage = clientRunner.initReconnectAuthentication(previousKey)
+ * sendToServer(clientMessage.getNextMessage());
+ * HandshakeMessage lastMessage = clientRunner.authenticateReconnection(previousKey, message)
+ * }
+ *
+ * {@code lastMessage.getHandshakeState()} should be FINISHED if reconnection handshake is done.
+ *
+ * <p>2. As a server.
+ *
+ * {@code
+ * byte[] initialMessage = getClientMessageBytes();
+ * HandshakeMessage message = serverRunner.respondToInitRequest(initialMessage);
+ * sendToClient(message.getNextMessage());
+ * byte[] clientMessage = getClientResponse();
+ * HandshakeMessage message = serverRunner.continueHandshake(clientMessage);}
+ *
+ * <p>if it is a first-time connection,
+ *
+ * {@code message.getHandshakeState()} should be VERIFICATION_NEEDED, show user the verification
+ * code and ask to verify.
+ * After PIN is confirmed, {@code HandshakeMessage lastMessage = serverRunner.verifyPin}, otherwise
+ * {@code clientRunner.invalidPin(); }
+ * Use {@code lastMessage.getKey()} to get the key for encryption.
+ *
+ * <p>If it is a reconnection,
+ *
+ * {@code message.getHandshakeState()} should be RESUMING_SESSION,PIN has been verified blindly,
+ * waiting for client message.
+ * After client message been received,
+ * {@code serverMessage = serverRunner.authenticateReconnection(previousKey, message);
+ * sendToClient(serverMessage.getNextMessage());}
+ * {@code serverMessage.getHandshakeState()} should be FINISHED if reconnection handshake is done.
+ *
+ * Also see {@link EncryptionRunnerTest} for examples.
+ */
+public interface EncryptionRunner {
+
+    String TAG = "EncryptionRunner";
+
+    /**
+     * Starts an encryption handshake.
+     *
+     * @return A handshake message with information about the handshake that is started.
+     */
+    @NonNull
+    HandshakeMessage initHandshake();
+
+    /**
+     * Starts an encryption handshake where the device that is being communicated with already
+     * initiated the request.
+     *
+     * @param initializationRequest the bytes that the other device sent over.
+     * @return a handshake message with information about the handshake.
+     * @throws HandshakeException if initialization request is invalid.
+     */
+    @NonNull
+    HandshakeMessage respondToInitRequest(@NonNull byte[] initializationRequest)
+            throws HandshakeException;
+
+    /**
+     * Continues a handshake after receiving another response from the connected device.
+     *
+     * @param response the response from the other device.
+     * @return a message that can be used to continue the handshake.
+     * @throws HandshakeException if unexpected bytes in response.
+     */
+    @NonNull
+    HandshakeMessage continueHandshake(@NonNull byte[] response) throws HandshakeException;
+
+    /**
+     * Verifies the pin shown to the user. The result is the next handshake message and will
+     * typically contain an encryption key.
+     *
+     * @throws HandshakeException if not in state to verify pin.
+     */
+    @NonNull
+    HandshakeMessage verifyPin() throws HandshakeException;
+
+    /**
+     * Notifies the encryption runner that the user failed to validate the pin. After calling this
+     * method the runner should not be used, and will throw exceptions.
+     */
+    void invalidPin();
+
+    /**
+     * Verifies the reconnection message.
+     *
+     * <p>The message passed to this method should have been generated by
+     * {@link #initReconnectAuthentication(byte[] previousKey)}.
+     *
+     * <p>If the message is valid, then a {@link HandshakeMessage} will be returned that contains
+     * the encryption key and a handshake message which can be used to verify the other side of the
+     * connection.
+     *
+     * @param previousKey previously stored key.
+     * @param message     message from the client
+     * @return a handshake message with an encryption key if verification succeed.
+     * @throws HandshakeException if the message does not match.
+     */
+    @NonNull
+    HandshakeMessage authenticateReconnection(@NonNull byte[] message, @NonNull byte[] previousKey)
+            throws HandshakeException;
+
+    /**
+     * Initiates the reconnection verification by generating a message that should be sent to the
+     * device that is being reconnected to.
+     *
+     * @param previousKey previously stored key.
+     * @return a handshake message with client's message which will be sent to server.
+     * @throws HandshakeException when get encryption key's unique session fail.
+     */
+    @NonNull
+    HandshakeMessage initReconnectAuthentication(@NonNull byte[] previousKey)
+            throws HandshakeException;
+
+    /**
+     * De-serializes a previously serialized key generated by an instance of this encryption runner.
+     *
+     * @param serialized the serialized bytes of the key.
+     * @return the Key object used for encryption.
+     */
+    @NonNull
+    Key keyOf(@NonNull byte[] serialized);
+
+    /**
+     * Set the signal if it is a reconnection process.
+     *
+     * @param isReconnect {@code true} if it is a reconnect.
+     */
+    void setIsReconnect(boolean isReconnect);
+}
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunnerFactory.java b/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunnerFactory.java
new file mode 100644
index 0000000..156abd8
--- /dev/null
+++ b/EncryptionRunner/src/android/car/encryptionrunner/EncryptionRunnerFactory.java
@@ -0,0 +1,45 @@
+/*
+ * 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 android.car.encryptionrunner;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Factory that creates encryption runner.
+ */
+public class EncryptionRunnerFactory {
+
+    private EncryptionRunnerFactory() {
+        // prevent instantiation.
+    }
+
+    /**
+     * Creates a new {@link EncryptionRunner}.
+     */
+    public static EncryptionRunner newRunner() {
+        return new Ukey2EncryptionRunner();
+    }
+
+    /**
+     * Creates a new {@link EncryptionRunner} one that doesn't actually do encryption but is useful
+     * for testing.
+     */
+    @VisibleForTesting
+    public static EncryptionRunner newDummyRunner() {
+        return new DummyEncryptionRunner();
+    }
+}
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/HandshakeException.java b/EncryptionRunner/src/android/car/encryptionrunner/HandshakeException.java
new file mode 100644
index 0000000..185a21c
--- /dev/null
+++ b/EncryptionRunner/src/android/car/encryptionrunner/HandshakeException.java
@@ -0,0 +1,31 @@
+/*
+ * 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 android.car.encryptionrunner;
+
+/**
+ * Exception indicating an error during a Handshake of EncryptionRunner.
+ */
+public class HandshakeException extends Exception {
+
+    HandshakeException(String message) {
+        super(message);
+    }
+
+    HandshakeException(Exception e) {
+        super(e);
+    }
+}
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/HandshakeMessage.java b/EncryptionRunner/src/android/car/encryptionrunner/HandshakeMessage.java
new file mode 100644
index 0000000..fa6705d
--- /dev/null
+++ b/EncryptionRunner/src/android/car/encryptionrunner/HandshakeMessage.java
@@ -0,0 +1,164 @@
+/*
+ * 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 android.car.encryptionrunner;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * During an {@link EncryptionRunner} handshake process, these are the messages returned as part
+ * of each step.
+ */
+public class HandshakeMessage {
+
+    /**
+     * States for handshake progress.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({HandshakeState.UNKNOWN, HandshakeState.IN_PROGRESS, HandshakeState.VERIFICATION_NEEDED,
+            HandshakeState.FINISHED, HandshakeState.INVALID, HandshakeState.RESUMING_SESSION,})
+    public @interface HandshakeState {
+        /**
+         * The initial state, this value is not expected to be returned.
+         */
+        int UNKNOWN = 0;
+        /**
+         * The handshake is in progress.
+         */
+        int IN_PROGRESS = 1;
+        /**
+         * The handshake is complete, but verification of the code is needed.
+         */
+        int VERIFICATION_NEEDED = 2;
+        /**
+         * The handshake is complete.
+         */
+        int FINISHED = 3;
+        /**
+         * The handshake is complete and not successful.
+         */
+        int INVALID = 4;
+        /**
+         * The handshake is complete, but extra verification is needed.
+         */
+        int RESUMING_SESSION = 5;
+    }
+
+    @HandshakeState
+    private final int mHandshakeState;
+    private final Key mKey;
+    private final byte[] mNextMessage;
+    private final String mVerificationCode;
+
+    /**
+     * @return Returns a builder for {@link HandshakeMessage}.
+     */
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * Use the builder;
+     */
+    private HandshakeMessage(
+            @HandshakeState int handshakeState,
+            @Nullable Key key,
+            @Nullable byte[] nextMessage,
+            @Nullable String verificationCode) {
+        mHandshakeState = handshakeState;
+        mKey = key;
+        mNextMessage = nextMessage;
+        mVerificationCode = verificationCode;
+    }
+
+    /**
+     * Returns the next message to send in a handshake.
+     */
+    @Nullable
+    public byte[] getNextMessage() {
+        return mNextMessage == null ? null : mNextMessage.clone();
+    }
+
+    /**
+     * Returns the state of the handshake.
+     */
+    @HandshakeState
+    public int getHandshakeState() {
+        return mHandshakeState;
+    }
+
+    /**
+     * Returns the encryption key that can be used to encrypt data.
+     */
+    @Nullable
+    public Key getKey() {
+        return mKey;
+    }
+
+    /**
+     * Returns a verification code to show to the user.
+     */
+    @Nullable
+    public String getVerificationCode() {
+        return mVerificationCode;
+    }
+
+    static class Builder {
+        @HandshakeState
+        int mHandshakeState;
+        Key mKey;
+        byte[] mNextMessage;
+        String mVerificationCode;
+
+        Builder setHandshakeState(@HandshakeState int handshakeState) {
+            mHandshakeState = handshakeState;
+            return this;
+        }
+
+        Builder setKey(@Nullable Key key) {
+            mKey = key;
+            return this;
+        }
+
+        Builder setNextMessage(@Nullable byte[] nextMessage) {
+            mNextMessage = nextMessage == null ? null : nextMessage.clone();
+            return this;
+        }
+
+        Builder setVerificationCode(@Nullable String verificationCode) {
+            mVerificationCode = verificationCode;
+            return this;
+        }
+
+        HandshakeMessage build() {
+            if (mHandshakeState == HandshakeState.UNKNOWN) {
+                throw new IllegalStateException("must set handshake state before calling build");
+            }
+            if (mHandshakeState == HandshakeState.VERIFICATION_NEEDED
+                    && TextUtils.isEmpty(mVerificationCode)) {
+                throw new IllegalStateException(
+                        "if state is verification needed, must have verification code");
+            }
+            return new HandshakeMessage(mHandshakeState, mKey, mNextMessage, mVerificationCode);
+        }
+
+    }
+}
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/Key.java b/EncryptionRunner/src/android/car/encryptionrunner/Key.java
new file mode 100644
index 0000000..2e32858
--- /dev/null
+++ b/EncryptionRunner/src/android/car/encryptionrunner/Key.java
@@ -0,0 +1,60 @@
+/*
+ * 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 android.car.encryptionrunner;
+
+import android.annotation.NonNull;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+
+/**
+ * Represents a serializable encryption key.
+ */
+public interface Key {
+    /**
+     * Returns a serialized encryption key.
+     */
+    @NonNull
+    byte[] asBytes();
+
+    /**
+     * Encrypts data using this key.
+     *
+     * @param data the data to be encrypted
+     * @return the encrypted data.
+     */
+    @NonNull
+    byte[] encryptData(@NonNull byte[] data);
+
+    /**
+     * Decrypts data using this key.
+     *
+     * @param encryptedData The encrypted data.
+     * @return decrypted data.
+     * @throws SignatureException if encrypted data is not properly signed.
+     */
+    @NonNull
+    byte[] decryptData(@NonNull byte[] encryptedData) throws SignatureException;
+
+    /**
+     * Returns a cryptographic digest of the key.
+     *
+     * @throws NoSuchAlgorithmException when a unique session can not be created.
+     */
+    @NonNull
+    byte[] getUniqueSession() throws NoSuchAlgorithmException;
+}
diff --git a/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java b/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
new file mode 100644
index 0000000..904d5c2
--- /dev/null
+++ b/EncryptionRunner/src/android/car/encryptionrunner/Ukey2EncryptionRunner.java
@@ -0,0 +1,397 @@
+/*
+ * 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 android.car.encryptionrunner;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.security.cryptauth.lib.securegcm.D2DConnectionContext;
+import com.google.security.cryptauth.lib.securegcm.Ukey2Handshake;
+import com.google.security.cryptauth.lib.securemessage.CryptoOps;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An {@link EncryptionRunner} that uses Ukey2 as the underlying implementation.
+ */
+public class Ukey2EncryptionRunner implements EncryptionRunner {
+
+    private static final Ukey2Handshake.HandshakeCipher CIPHER =
+            Ukey2Handshake.HandshakeCipher.P256_SHA512;
+    private static final int RESUME_HMAC_LENGTH = 32;
+    private static final byte[] RESUME = "RESUME".getBytes();
+    private static final byte[] SERVER = "SERVER".getBytes();
+    private static final byte[] CLIENT = "CLIENT".getBytes();
+    private static final int AUTH_STRING_LENGTH = 6;
+
+    @IntDef({Mode.UNKNOWN, Mode.CLIENT, Mode.SERVER})
+    private @interface Mode {
+        int UNKNOWN = 0;
+        int CLIENT = 1;
+        int SERVER = 2;
+    }
+
+    private Ukey2Handshake mUkey2client;
+    private boolean mRunnerIsInvalid;
+    private Key mCurrentKey;
+    private byte[] mCurrentUniqueSesion;
+    private byte[] mPrevUniqueSesion;
+    private boolean mIsReconnect;
+    private boolean mInitReconnectionVerification;
+    @Mode
+    private int mMode = Mode.UNKNOWN;
+
+    @Override
+    public HandshakeMessage initHandshake() {
+        checkRunnerIsNew();
+        mMode = Mode.CLIENT;
+        try {
+            mUkey2client = Ukey2Handshake.forInitiator(CIPHER);
+            return HandshakeMessage.newBuilder()
+                    .setHandshakeState(getHandshakeState())
+                    .setNextMessage(mUkey2client.getNextHandshakeMessage())
+                    .build();
+        } catch (com.google.security.cryptauth.lib.securegcm.HandshakeException e) {
+            Log.e(TAG, "unexpected exception", e);
+            throw new RuntimeException(e);
+        }
+
+    }
+
+    @Override
+    public void setIsReconnect(boolean isReconnect) {
+        mIsReconnect = isReconnect;
+    }
+
+    @Override
+    public HandshakeMessage respondToInitRequest(byte[] initializationRequest)
+            throws HandshakeException {
+        checkRunnerIsNew();
+        mMode = Mode.SERVER;
+        try {
+            if (mUkey2client != null) {
+                throw new IllegalStateException("Cannot reuse encryption runners, "
+                        + "this one is already initialized");
+            }
+            mUkey2client = Ukey2Handshake.forResponder(CIPHER);
+            mUkey2client.parseHandshakeMessage(initializationRequest);
+            return HandshakeMessage.newBuilder()
+                    .setHandshakeState(getHandshakeState())
+                    .setNextMessage(mUkey2client.getNextHandshakeMessage())
+                    .build();
+
+        } catch (com.google.security.cryptauth.lib.securegcm.HandshakeException
+                | Ukey2Handshake.AlertException e) {
+            throw new HandshakeException(e);
+        }
+    }
+
+    private void checkRunnerIsNew() {
+        if (mUkey2client != null) {
+            throw new IllegalStateException("This runner is already initialized.");
+        }
+    }
+
+
+    @Override
+    public HandshakeMessage continueHandshake(byte[] response) throws HandshakeException {
+        checkInitialized();
+        try {
+            if (mUkey2client.getHandshakeState() != Ukey2Handshake.State.IN_PROGRESS) {
+                throw new IllegalStateException("handshake is not in progress, state ="
+                        + mUkey2client.getHandshakeState());
+            }
+            mUkey2client.parseHandshakeMessage(response);
+
+            // Not obvious from ukey2 api, but getting the next message can change the state.
+            // calling getNext message might go from in progress to verification needed, on
+            // the assumption that we already send this message to the peer.
+            byte[] nextMessage = null;
+            if (mUkey2client.getHandshakeState() == Ukey2Handshake.State.IN_PROGRESS) {
+                nextMessage = mUkey2client.getNextHandshakeMessage();
+            }
+            String verificationCode = null;
+            if (mUkey2client.getHandshakeState() == Ukey2Handshake.State.VERIFICATION_NEEDED) {
+                // getVerificationString() needs to be called before verifyPin().
+                verificationCode = generateReadablePairingCode(
+                        mUkey2client.getVerificationString(AUTH_STRING_LENGTH));
+                if (mIsReconnect) {
+                    HandshakeMessage handshakeMessage = verifyPin();
+                    return HandshakeMessage.newBuilder()
+                            .setHandshakeState(handshakeMessage.getHandshakeState())
+                            .setNextMessage(nextMessage)
+                            .build();
+                }
+            }
+            return HandshakeMessage.newBuilder()
+                    .setHandshakeState(getHandshakeState())
+                    .setNextMessage(nextMessage)
+                    .setVerificationCode(verificationCode)
+                    .build();
+        } catch (com.google.security.cryptauth.lib.securegcm.HandshakeException
+                | Ukey2Handshake.AlertException e) {
+            throw new HandshakeException(e);
+        }
+    }
+
+    /**
+     * Returns a human-readable pairing code string generated from the verification bytes. Converts
+     * each byte into a digit with a simple modulo.
+     *
+     * <p>This should match the implementation in the iOS and Android client libraries.
+     */
+    @VisibleForTesting
+    String generateReadablePairingCode(byte[] verificationCode) {
+        StringBuilder outString = new StringBuilder();
+        for (byte b : verificationCode) {
+            int unsignedInt = Byte.toUnsignedInt(b);
+            int digit = unsignedInt % 10;
+            outString.append(digit);
+        }
+
+        return outString.toString();
+    }
+
+    private static class UKey2Key implements Key {
+
+        private final D2DConnectionContext mConnectionContext;
+
+        UKey2Key(@NonNull D2DConnectionContext connectionContext) {
+            this.mConnectionContext = connectionContext;
+        }
+
+        @Override
+        public byte[] asBytes() {
+            return mConnectionContext.saveSession();
+        }
+
+        @Override
+        public byte[] encryptData(byte[] data) {
+            return mConnectionContext.encodeMessageToPeer(data);
+        }
+
+        @Override
+        public byte[] decryptData(byte[] encryptedData) throws SignatureException {
+            return mConnectionContext.decodeMessageFromPeer(encryptedData);
+        }
+
+        @Override
+        public byte[] getUniqueSession() throws NoSuchAlgorithmException {
+            return mConnectionContext.getSessionUnique();
+        }
+    }
+
+    @Override
+    public HandshakeMessage verifyPin() throws HandshakeException {
+        checkInitialized();
+        mUkey2client.verifyHandshake();
+        int state = getHandshakeState();
+        try {
+            mCurrentKey = new UKey2Key(mUkey2client.toConnectionContext());
+        } catch (com.google.security.cryptauth.lib.securegcm.HandshakeException e) {
+            throw new HandshakeException(e);
+        }
+        return HandshakeMessage.newBuilder()
+                .setHandshakeState(state)
+                .setKey(mCurrentKey)
+                .build();
+    }
+
+    /**
+     * <p>After getting message from the other device, authenticate the message with the previous
+     * stored key.
+     *
+     * If current device inits the reconnection authentication by calling {@code
+     * initReconnectAuthentication} and sends the message to the other device, the other device
+     * will call {@code authenticateReconnection()} with the received message and send its own
+     * message back to the init device. The init device will call {@code
+     * authenticateReconnection()} on the received message, but do not need to set the next
+     * message.
+     */
+    @Override
+    public HandshakeMessage authenticateReconnection(byte[] message, byte[] previousKey)
+            throws HandshakeException {
+        if (!mIsReconnect) {
+            throw new HandshakeException(
+                    "Reconnection authentication requires setIsReconnect(true)");
+        }
+        if (mCurrentKey == null) {
+            throw new HandshakeException("Current key is null, make sure verifyPin() is called.");
+        }
+        if (message.length != RESUME_HMAC_LENGTH) {
+            mRunnerIsInvalid = true;
+            throw new HandshakeException("Failing because (message.length =" + message.length
+                    + ") is not equal to " + RESUME_HMAC_LENGTH);
+        }
+        try {
+            mCurrentUniqueSesion = mCurrentKey.getUniqueSession();
+            mPrevUniqueSesion = keyOf(previousKey).getUniqueSession();
+        } catch (NoSuchAlgorithmException e) {
+            throw new HandshakeException(e);
+        }
+        switch (mMode) {
+            case Mode.SERVER:
+                if (!MessageDigest.isEqual(
+                        message, computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, CLIENT))) {
+                    mRunnerIsInvalid = true;
+                    throw new HandshakeException("Reconnection authentication failed.");
+                }
+                return HandshakeMessage.newBuilder()
+                        .setHandshakeState(HandshakeMessage.HandshakeState.FINISHED)
+                        .setKey(mCurrentKey)
+                        .setNextMessage(mInitReconnectionVerification ? null
+                                : computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, SERVER))
+                        .build();
+            case Mode.CLIENT:
+                if (!MessageDigest.isEqual(
+                        message, computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, SERVER))) {
+                    mRunnerIsInvalid = true;
+                    throw new HandshakeException("Reconnection authentication failed.");
+                }
+                return HandshakeMessage.newBuilder()
+                        .setHandshakeState(HandshakeMessage.HandshakeState.FINISHED)
+                        .setKey(mCurrentKey)
+                        .setNextMessage(mInitReconnectionVerification ? null
+                                : computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, CLIENT))
+                        .build();
+            default:
+                throw new IllegalStateException(
+                        "Encountered unexpected role during authenticateReconnection: " + mMode);
+        }
+    }
+
+    /**
+     * Both client and server can call this method to send authentication message to the other
+     * device.
+     */
+    @Override
+    public HandshakeMessage initReconnectAuthentication(byte[] previousKey)
+            throws HandshakeException {
+        if (!mIsReconnect) {
+            throw new HandshakeException(
+                    "Reconnection authentication requires setIsReconnect(true).");
+        }
+        if (mCurrentKey == null) {
+            throw new HandshakeException("Current key is null, make sure verifyPin() is called.");
+        }
+        mInitReconnectionVerification = true;
+        try {
+            mCurrentUniqueSesion = mCurrentKey.getUniqueSession();
+            mPrevUniqueSesion = keyOf(previousKey).getUniqueSession();
+        } catch (NoSuchAlgorithmException e) {
+            throw new HandshakeException(e);
+        }
+        switch (mMode) {
+            case Mode.SERVER:
+                return HandshakeMessage.newBuilder()
+                        .setHandshakeState(HandshakeMessage.HandshakeState.RESUMING_SESSION)
+                        .setNextMessage(computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, SERVER))
+                        .build();
+            case Mode.CLIENT:
+                return HandshakeMessage.newBuilder()
+                        .setHandshakeState(HandshakeMessage.HandshakeState.RESUMING_SESSION)
+                        .setNextMessage(computeMAC(mPrevUniqueSesion, mCurrentUniqueSesion, CLIENT))
+                        .build();
+            default:
+                throw new IllegalStateException(
+                        "Encountered unexpected role during authenticateReconnection: " + mMode);
+        }
+    }
+
+    @HandshakeMessage.HandshakeState
+    private int getHandshakeState() {
+        checkInitialized();
+        switch (mUkey2client.getHandshakeState()) {
+            case ALREADY_USED:
+            case ERROR:
+                throw new IllegalStateException("unexpected error state");
+            case FINISHED:
+                if (mIsReconnect) {
+                    return HandshakeMessage.HandshakeState.RESUMING_SESSION;
+                }
+                return HandshakeMessage.HandshakeState.FINISHED;
+            case IN_PROGRESS:
+                return HandshakeMessage.HandshakeState.IN_PROGRESS;
+            case VERIFICATION_IN_PROGRESS:
+            case VERIFICATION_NEEDED:
+                return HandshakeMessage.HandshakeState.VERIFICATION_NEEDED;
+            default:
+                throw new IllegalStateException("unexpected handshake state");
+        }
+    }
+
+    @Override
+    public Key keyOf(byte[] serialized) {
+        return new UKey2Key(D2DConnectionContext.fromSavedSession(serialized));
+    }
+
+    @Override
+    public void invalidPin() {
+        mRunnerIsInvalid = true;
+    }
+
+    private UKey2Key checkIsUkey2Key(Key key) {
+        if (!(key instanceof UKey2Key)) {
+            throw new IllegalArgumentException("wrong key type");
+        }
+        return (UKey2Key) key;
+    }
+
+    private void checkInitialized() {
+        if (mUkey2client == null) {
+            throw new IllegalStateException("runner not initialized");
+        }
+        if (mRunnerIsInvalid) {
+            throw new IllegalStateException("runner has been invalidated");
+        }
+    }
+
+    @Nullable
+    private byte[] computeMAC(byte[] previous, byte[] next, byte[] info) {
+        try {
+            SecretKeySpec inputKeyMaterial = new SecretKeySpec(
+                    concatByteArrays(previous, next), "" /* key type is just plain raw bytes */);
+            return CryptoOps.hkdf(inputKeyMaterial, RESUME, info);
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            // Does not happen in practice
+            Log.e(TAG, "Compute MAC failed");
+            return null;
+        }
+    }
+
+    private static byte[] concatByteArrays(@NonNull byte[] a, @NonNull byte[] b) {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            outputStream.write(a);
+            outputStream.write(b);
+        } catch (IOException e) {
+            return new byte[0];
+        }
+        return outputStream.toByteArray();
+    }
+}
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index f8d8dac..a810f8e 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -2,7 +2,7 @@
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
 ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
 chassis_current_hook = car-ui-lib/tests/apitest/auto-generate-resources.py --sha ${PREUPLOAD_COMMIT} --compare
-
+chassis_findviewbyid_check = car-ui-lib/findviewbyid-preupload-hook.sh
 [Builtin Hooks]
 commit_msg_changeid_field = true
 commit_msg_test_field = true
diff --git a/androidx-room/Android.bp b/androidx-room/Android.bp
new file mode 100644
index 0000000..f9ac357
--- /dev/null
+++ b/androidx-room/Android.bp
@@ -0,0 +1,56 @@
+//
+// 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_plugin {
+    name: "car-androidx-room-compiler",
+    static_libs: [
+        "car-androidx-annotation-nodeps-bp",
+        "car-androidx-room-common-nodeps-bp",
+        "car-androidx-room-compiler-nodeps-bp",
+        "car-androidx-room-compiler-tools-common-m2-deps",
+        "car-androidx-room-migration-nodeps-bp",
+        "kotlin-stdlib",
+    ],
+    processor_class: "androidx.room.RoomProcessor",
+    generates_api: true,
+}
+
+android_library_import {
+    name: "car-androidx-room-runtime-nodeps-bp",
+    aars: ["androidx.room/room-runtime-2.0.0-alpha1.aar"],
+    sdk_version: "current",
+}
+
+java_import {
+    name: "car-androidx-room-common-nodeps-bp",
+    jars: ["androidx.room/room-common-2.0.0-alpha1.jar"],
+    host_supported: true,
+}
+
+java_import_host {
+    name: "car-androidx-room-compiler-nodeps-bp",
+    jars: ["androidx.room/room-compiler-2.0.0-alpha1.jar"],
+}
+
+java_import_host {
+    name: "car-androidx-room-migration-nodeps-bp",
+    jars: ["androidx.room/room-migration-2.0.0-alpha1.jar"],
+}
+
+java_import_host {
+    name: "car-androidx-annotation-nodeps-bp",
+    jars: ["annotation-1.0.0-alpha1.jar"],
+}
diff --git a/car-apps-common/Android.bp b/car-apps-common/Android.bp
new file mode 100644
index 0000000..8c52089
--- /dev/null
+++ b/car-apps-common/Android.bp
@@ -0,0 +1,39 @@
+// 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: "car-apps-common-bp",
+
+    srcs: ["src/**/*.java"],
+
+    resource_dirs: ["res"],
+
+    optimize: {
+        enabled: false,
+    },
+
+    libs: ["android.car"],
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.cardview_cardview",
+        "androidx.interpolator_interpolator",
+        "androidx.lifecycle_lifecycle-common-java8",
+        "androidx.lifecycle_lifecycle-extensions",
+        "androidx-constraintlayout_constraintlayout",
+        "androidx.recyclerview_recyclerview",
+        "androidx-constraintlayout_constraintlayout-solver",
+    ],
+}
diff --git a/car-apps-common/res/layout/control_bar.xml b/car-apps-common/res/layout/control_bar.xml
index d800565..a24038a 100644
--- a/car-apps-common/res/layout/control_bar.xml
+++ b/car-apps-common/res/layout/control_bar.xml
@@ -23,6 +23,7 @@
     <LinearLayout
         android:id="@+id/rows_container"
         android:orientation="vertical"
+        android:layoutDirection="ltr"
         android:layout_gravity="center"
         android:layout_width="match_parent"
         android:layout_height="wrap_content">
diff --git a/car-apps-common/res/layout/minimized_control_bar.xml b/car-apps-common/res/layout/minimized_control_bar.xml
index f91941c..814fe15 100644
--- a/car-apps-common/res/layout/minimized_control_bar.xml
+++ b/car-apps-common/res/layout/minimized_control_bar.xml
@@ -68,6 +68,9 @@
         app:layout_constraintStart_toStartOf="@+id/minimized_control_bar_title"
         app:layout_constraintEnd_toEndOf="@+id/minimized_control_bar_title"/>
 
+    <!-- Using a LinearLayout as a wrapper to be able to define layout constraints between the
+    parent (ConstraintLayout with locale based layout) and the child (LinearLayout with LTR
+    layout).  -->
     <LinearLayout
         android:id="@+id/minimized_control_buttons_wrapper"
         android:layout_width="wrap_content"
@@ -79,31 +82,38 @@
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintStart_toEndOf="@+id/minimized_control_bar_title"
         app:layout_constraintEnd_toEndOf="parent">
-        <include
-            android:id="@+id/minimized_control_bar_left_slot"
-            android:layout_width="@dimen/minimized_control_bar_button_size"
-            android:layout_height="@dimen/minimized_control_bar_button_size"
-            android:layout_marginEnd="@dimen/minimized_control_bar_button_padding"
-            android:layout_gravity="center_vertical"
-            android:layout_weight="1"
-            layout="@layout/control_bar_slot"/>
-        <include
-            android:id="@+id/minimized_control_bar_main_slot"
-            android:layout_width="@dimen/minimized_control_bar_button_size"
-            android:layout_height="@dimen/minimized_control_bar_button_size"
-            android:layout_gravity="center_vertical"
-            android:layout_weight="1"
-            layout="@layout/control_bar_slot"/>
-        <include
-            android:id="@+id/minimized_control_bar_right_slot"
-            android:layout_width="@dimen/minimized_control_bar_button_size"
-            android:layout_height="@dimen/minimized_control_bar_button_size"
-            android:layout_marginStart="@dimen/minimized_control_bar_button_padding"
-            android:layout_gravity="center_vertical"
-            android:layout_weight="1"
-            layout="@layout/control_bar_slot"/>
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:clipChildren="false"
+            android:layoutDirection="ltr"
+            android:layout_gravity="center_vertical">
+            <include
+                android:id="@+id/minimized_control_bar_left_slot"
+                android:layout_width="@dimen/minimized_control_bar_button_size"
+                android:layout_height="@dimen/minimized_control_bar_button_size"
+                android:layout_marginEnd="@dimen/minimized_control_bar_button_padding"
+                android:layout_gravity="center_vertical"
+                android:layout_weight="1"
+                layout="@layout/control_bar_slot"/>
+            <include
+                android:id="@+id/minimized_control_bar_main_slot"
+                android:layout_width="@dimen/minimized_control_bar_button_size"
+                android:layout_height="@dimen/minimized_control_bar_button_size"
+                android:layout_gravity="center_vertical"
+                android:layout_weight="1"
+                layout="@layout/control_bar_slot"/>
+            <include
+                android:id="@+id/minimized_control_bar_right_slot"
+                android:layout_width="@dimen/minimized_control_bar_button_size"
+                android:layout_height="@dimen/minimized_control_bar_button_size"
+                android:layout_marginStart="@dimen/minimized_control_bar_button_padding"
+                android:layout_gravity="center_vertical"
+                android:layout_weight="1"
+                layout="@layout/control_bar_slot"/>
+        </LinearLayout>
     </LinearLayout>
 
 
-
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/car-apps-common/res/values-zh-rCN/strings.xml b/car-apps-common/res/values-zh-rCN/strings.xml
index 899f786..2c14a4d 100644
--- a/car-apps-common/res/values-zh-rCN/strings.xml
+++ b/car-apps-common/res/values-zh-rCN/strings.xml
@@ -16,8 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- no translation found for control_bar_expand_collapse_button (3420351169078117938) -->
-    <skip />
+    <string name="control_bar_expand_collapse_button" msgid="3420351169078117938">"“展开”/“收起”按钮"</string>
     <string name="car_drawer_open" msgid="2676372472514742324">"打开抽屉式导航栏"</string>
     <string name="car_drawer_close" msgid="5329374630462464855">"关闭抽屉式导航栏"</string>
     <string name="restricted_while_driving" msgid="2278031053760704437">"驾车时无法使用此功能。"</string>
diff --git a/car-apps-common/res/values/styles.xml b/car-apps-common/res/values/styles.xml
index 28c3584..6983bdc 100644
--- a/car-apps-common/res/values/styles.xml
+++ b/car-apps-common/res/values/styles.xml
@@ -101,8 +101,12 @@
         <item name="android:letterSpacing">@dimen/letter_spacing_body3</item>
     </style>
 
-    <style name="MinimizedControlBarTitle" parent="TextAppearance.Body1"/>
-    <style name="MinimizedControlBarSubtitle" parent="TextAppearance.Body3"/>
+    <style name="MinimizedControlBarTitle" parent="TextAppearance.Body1">
+        <item name="android:textDirection">locale</item>
+    </style>
+    <style name="MinimizedControlBarSubtitle" parent="TextAppearance.Body3">
+        <item name="android:textDirection">locale</item>
+    </style>
 
     <!-- Styles for ControlBar -->
     <style name="ControlBar">
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java b/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
index 9b13438..ee50b72 100644
--- a/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
@@ -27,7 +27,15 @@
 import com.android.car.apps.common.R;
 
 /**
- * Binds images to an image view.
+ * Binds images to an image view.<p/>
+ * While making a new image request (including passing a null {@link ImageBinder.ImageRef} in
+ * {@link #setImage}) will cancel the current image request (if any), RecyclerView doesn't
+ * always reuse all its views, causing multiple requests to not be canceled. On a slow network,
+ * those requests then take time to execute and can make it look like the application has
+ * stopped loading images if the user keeps browsing. To prevent that, override:
+ * {@link RecyclerView.Adapter#onViewDetachedFromWindow} and call {@link #maybeCancelLoading}
+ * {@link RecyclerView.Adapter#onViewAttachedToWindow} and call {@link #maybeRestartLoading}.
+ *
  * @param <T> see {@link ImageRef}.
  */
 public class ImageViewBinder<T extends ImageBinder.ImageRef> extends ImageBinder<T> {
@@ -36,6 +44,9 @@
     private final ImageView mImageView;
     private final boolean mFlagBitmaps;
 
+    private T mSavedRef;
+    private boolean mCancelled;
+
     /** See {@link ImageViewBinder} and {@link ImageBinder}. */
     public ImageViewBinder(Size maxImageSize, @Nullable ImageView imageView) {
         this(PlaceholderType.FOREGROUND, maxImageSize, imageView, false);
@@ -71,13 +82,39 @@
         }
     }
 
+    /**
+     * Loads a new {@link ImageRef}. The previous request (if any) will be canceled.
+     */
     @Override
     public void setImage(Context context, @Nullable T newRef) {
+        mSavedRef = newRef;
+        mCancelled = false;
         if (mImageView != null) {
             super.setImage(context, newRef);
         }
     }
 
+    /**
+     * Restarts the image loading request if {@link #setImage} was called with a valid reference
+     * that could not be loaded before {@link #maybeCancelLoading} was called.
+     */
+    public void maybeRestartLoading(Context context) {
+        if (mCancelled) {
+            setImage(context, mSavedRef);
+        }
+    }
+
+    /**
+     * Cancels the current loading request (if any) so it doesn't take cycles when the imageView
+     * doesn't need the image (like when the view was moved off screen).
+     */
+    public void maybeCancelLoading(Context context) {
+        mCancelled = true;
+        if (mImageView != null) {
+            super.setImage(context, null); // Call super to keep mSavedRef.
+        }
+    }
+
     @Override
     protected void prepareForNewBinding(Context context) {
         mImageView.setImageBitmap(null);
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java b/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
index dc10caf..e8b245e 100644
--- a/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
@@ -62,6 +62,7 @@
 
     private static final String TAG = "LocalImageFetcher";
     private static final boolean L_WARN = Log.isLoggable(TAG, Log.WARN);
+    private static final boolean L_DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int KB = 1024;
     private static final int MB = KB * KB;
@@ -150,6 +151,9 @@
                 task = new ImageLoadingTask(context, key, mFlagRemoteImages);
                 mTasks.put(key, task);
                 task.executeOnExecutor(getThreadPool(packageName));
+                if (L_DEBUG) {
+                    Log.d(TAG, "Added task " + key.mImageUri);
+                }
             } else {
                 Log.e(TAG, "No package for " + key.mImageUri);
             }
@@ -168,8 +172,10 @@
                 ImageLoadingTask task = mTasks.remove(key);
                 if (task != null) {
                     task.cancel(true);
-                }
-                if (L_WARN) {
+                    if (L_DEBUG) {
+                        Log.d(TAG, "Canceled task " + key.mImageUri);
+                    }
+                } else if (L_WARN) {
                     Log.w(TAG, "cancelRequest missing task for: " + key);
                 }
             }
@@ -325,6 +331,10 @@
         @UiThread
         @Override
         protected void onPostExecute(Drawable drawable) {
+            if (L_DEBUG) {
+                Log.d(TAG, "onPostExecute canceled:  " + isCancelled() + " drawable: " + drawable
+                        + " " + mImageKey.mImageUri);
+            }
             if (!isCancelled()) {
                 if (sInstance != null) {
                     sInstance.fulfilRequests(this, drawable);
diff --git a/car-apps-common/src/com/android/car/apps/common/util/SafeLog.java b/car-apps-common/src/com/android/car/apps/common/util/SafeLog.java
new file mode 100644
index 0000000..a3d10e4
--- /dev/null
+++ b/car-apps-common/src/com/android/car/apps/common/util/SafeLog.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.apps.common.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+/**
+ * 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/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 30acf61..fe11f38 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
@@ -164,6 +164,13 @@
         }
     }
 
+    /** Sets the activated state of the (optional) view. */
+    public static void setActivated(@Nullable View view, boolean activated) {
+        if (view != null) {
+            view.setActivated(activated);
+        }
+    }
+
     /** Sets onClickListener for the (optional) view. */
     public static void setOnClickListener(@Nullable View view, @Nullable View.OnClickListener l) {
         if (view != null) {
diff --git a/car-assist-client-lib/res/values/config.xml b/car-assist-client-lib/res/values/config.xml
index 561054c..16ceea5 100644
--- a/car-assist-client-lib/res/values/config.xml
+++ b/car-assist-client-lib/res/values/config.xml
@@ -16,5 +16,5 @@
 -->
 <resources>
     <!-- Whether FallbackAssistant is enabled. -->
-    <bool name="config_enableFallbackAssistant">true</bool>
+    <bool name="config_enableFallbackAssistant">false</bool>
 </resources>
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/FallbackAssistant.java b/car-assist-client-lib/src/com/android/car/assist/client/FallbackAssistant.java
index ee2aabc..db13ab1 100644
--- a/car-assist-client-lib/src/com/android/car/assist/client/FallbackAssistant.java
+++ b/car-assist-client-lib/src/com/android/car/assist/client/FallbackAssistant.java
@@ -122,13 +122,18 @@
             listener.onMessageRead(/* hasError= */ true);
             return;
         }
-        // The sender should be the same for all the messages.
-        Person sender = messageList.get(0).getSenderPerson();
-        if (sender != null) {
-            messages.add(sender.getName());
+
+        Person previousSender = messageList.get(0).getSenderPerson();
+        if (previousSender != null) {
+            messages.add(previousSender.getName());
             messages.add(mVerbForSays);
         }
         for (Message message : messageList) {
+            if (!message.getSenderPerson().equals(previousSender)) {
+                messages.add(message.getSenderPerson().getName());
+                messages.add(mVerbForSays);
+                previousSender = message.getSenderPerson();
+            }
             messages.add(message.getText());
         }
 
diff --git a/car-media-common/res/color/playback_control_color.xml b/car-media-common/res/color/playback_control_color.xml
index 73655f7..4a946f7 100644
--- a/car-media-common/res/color/playback_control_color.xml
+++ b/car-media-common/res/color/playback_control_color.xml
@@ -15,5 +15,6 @@
   limitations under the License.
 -->
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:color="@color/icon_tint" />
+    <item android:state_enabled="false" android:color="@color/media_button_tint_disabled"/>
+    <item android:color="@color/media_button_tint" />
 </selector>
diff --git a/car-media-common/res/values/colors.xml b/car-media-common/res/values/colors.xml
index 6f9e489..51927a7 100644
--- a/car-media-common/res/values/colors.xml
+++ b/car-media-common/res/values/colors.xml
@@ -39,6 +39,9 @@
     <!-- Color used on the fab spinner -->
     <color name="fab_spinner_indeterminate_color">@color/media_source_default_color</color>
 
+    <!-- Color used on media control buttons -->
+    <color name="media_button_tint">#FFFFFFFF</color>
+    <color name="media_button_tint_disabled">#80FFFFFF</color>
 
     <color name="placeholder_color_0">#669DF6</color>
     <color name="placeholder_color_1">#667EF6</color>
diff --git a/car-media-common/res/values/config.xml b/car-media-common/res/values/config.xml
index 5cf1bc2..34c1eb6 100644
--- a/car-media-common/res/values/config.xml
+++ b/car-media-common/res/values/config.xml
@@ -25,4 +25,13 @@
     <string name="launcher_intent" translatable="false">
         intent:#Intent;component=com.android.car.carlauncher/.AppGridActivity;launchFlags=0x24000000;S.com.android.car.carlauncher.mode=MEDIA_ONLY;end
     </string>
-</resources>
\ No newline at end of file
+
+    <!-- A list of custom media component names, which are created by calling
+     ComponentName#flattenToString(). Those components won't be shown
+     in the launcher because their applications' launcher activities will be
+     shown. Those components won't be opened by Media Center, and their
+     launcher activities will be launched directly instead. -->
+    <string-array name="custom_media_packages" translatable="false">
+        <item>com.android.car.radio/com.android.car.radio.service.RadioAppService</item>
+    </string-array>
+</resources>
diff --git a/car-media-common/res/values/styles.xml b/car-media-common/res/values/styles.xml
index 972837d..5d00241 100644
--- a/car-media-common/res/values/styles.xml
+++ b/car-media-common/res/values/styles.xml
@@ -18,11 +18,13 @@
     <style name="PlaybackTitleStyle" parent="TextAppearance.Body1">
         <item name="android:singleLine">true</item>
         <item name="android:includeFontPadding">false</item>
+        <item name="android:textDirection">locale</item>
     </style>
 
     <style name="PlaybackSubtitleStyle" parent="TextAppearance.Body3">
         <item name="android:textColor">@color/secondary_text_color</item>
         <item name="android:singleLine">true</item>
         <item name="android:includeFontPadding">false</item>
+        <item name="android:textDirection">locale</item>
     </style>
 </resources>
diff --git a/car-media-common/src/com/android/car/media/common/MediaButtonController.java b/car-media-common/src/com/android/car/media/common/MediaButtonController.java
index b7bfa19..c281ff7 100644
--- a/car-media-common/src/com/android/car/media/common/MediaButtonController.java
+++ b/car-media-common/src/com/android/car/media/common/MediaButtonController.java
@@ -53,8 +53,6 @@
 public class MediaButtonController {
 
     private static final String TAG = "MediaButton";
-    private static final float ALPHA_ENABLED = 1.0F;
-    private static final float ALPHA_DISABLED = 0.5F;
 
     private Context mContext;
     private PlayPauseStopImageView mPlayPauseStopImageView;
@@ -175,12 +173,6 @@
             mControlBar.setView(null, ControlBar.SLOT_LEFT);
             mSkipPrevAdded = false;
         }
-
-        if (skipPreviousEnabled) {
-            mSkipPrevButton.setAlpha(ALPHA_ENABLED);
-        } else {
-            mSkipPrevButton.setAlpha(ALPHA_DISABLED);
-        }
         mSkipPrevButton.setEnabled(skipPreviousEnabled);
 
         boolean skipNextReserved = hasState && state.isSkipNextReserved();
@@ -195,12 +187,6 @@
             mControlBar.setView(null, ControlBar.SLOT_RIGHT);
             mSkipNextAdded = false;
         }
-
-        if (skipNextEnabled) {
-            mSkipNextButton.setAlpha(ALPHA_ENABLED);
-        } else {
-            mSkipNextButton.setAlpha(ALPHA_DISABLED);
-        }
         mSkipNextButton.setEnabled(skipNextEnabled);
 
         updateCustomActions(state);
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 80a00f2..95c0dd5 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
@@ -299,7 +299,7 @@
                             .collect(Collectors.toList());
 
             mSanitizedQueue.setValue(filtered);
-            mHasQueue.setValue(!filtered.isEmpty());
+            mHasQueue.setValue(filtered.size() > 1);
         }
 
         @Override
@@ -615,8 +615,9 @@
          */
         public void playItem(MediaItemMetadata item) {
             if (mMediaController != null) {
-                mMediaController.getTransportControls().playFromMediaId(item.getId(),
-                        item.getExtras());
+                // Do NOT pass the extras back as that's not the official API and isn't supported
+                // in media2, so apps should not rely on this.
+                mMediaController.getTransportControls().playFromMediaId(item.getId(), null);
             }
         }
 
diff --git a/car-messenger-common/Android.bp b/car-messenger-common/Android.bp
new file mode 100644
index 0000000..19ed50f
--- /dev/null
+++ b/car-messenger-common/Android.bp
@@ -0,0 +1,37 @@
+//
+// 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: "car-messenger-common",
+
+    srcs: ["src/**/*.java"],
+
+    optimize: {
+        enabled: false,
+    },
+
+    libs: ["android.car"],
+
+    resource_dirs: ["res"],
+
+    static_libs: [
+        "android.car.userlib",
+        "androidx.legacy_legacy-support-v4",
+        "car-apps-common-bp",
+        "car-messenger-protos",
+        "connected-device-protos",
+    ],
+}
diff --git a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml b/car-messenger-common/AndroidManifest.xml
similarity index 66%
copy from car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
copy to car-messenger-common/AndroidManifest.xml
index 19a1111..148737d 100644
--- a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
+++ b/car-messenger-common/AndroidManifest.xml
@@ -14,12 +14,8 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <com.android.car.ui.toolbar.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-</FrameLayout>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.messenger.common">
+    <uses-permission android:name="android.car.permission.ACCESS_CAR_PROJECTION_STATUS"/>
+</manifest>
diff --git a/car-messenger-common/proto/Android.bp b/car-messenger-common/proto/Android.bp
new file mode 100644
index 0000000..c08e72b
--- /dev/null
+++ b/car-messenger-common/proto/Android.bp
@@ -0,0 +1,26 @@
+//
+// 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: "car-messenger-protos",
+    host_supported: true,
+    proto: {
+        type: "lite",
+    },
+    srcs: ["*.proto"],
+    jarjar_rules: "jarjar-rules.txt",
+    sdk_version: "28",
+}
diff --git a/car-messenger-common/proto/jarjar-rules.txt b/car-messenger-common/proto/jarjar-rules.txt
new file mode 100644
index 0000000..d27aecb
--- /dev/null
+++ b/car-messenger-common/proto/jarjar-rules.txt
@@ -0,0 +1 @@
+rule com.google.protobuf.** com.android.car.protobuf.@1
diff --git a/car-messenger-common/proto/notification_msg.proto b/car-messenger-common/proto/notification_msg.proto
new file mode 100644
index 0000000..d3b87ab
--- /dev/null
+++ b/car-messenger-common/proto/notification_msg.proto
@@ -0,0 +1,220 @@
+/*
+ * 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.messenger.proto;
+
+option java_package = "com.android.car.messenger.NotificationMsgProto";
+
+// Message to be sent from the phone SDK to the IHU SDK.
+message PhoneToCarMessage {
+  // The unique key of the message notification, same in phone and car.
+  // This will be the StatusBarNotification id of the original message
+  // notification posted on the phone.
+  string notification_key = 1;
+
+  // The different types of messages to be sent from the phone SDK.
+  oneof message_data {
+    // Metadata of a new conversation (new in the history of the current
+    // connection between phone and IHU SDKs).
+    ConversationNotification conversation = 2;
+    // Metadata of a new conversation received in an existing conversation.
+    MessagingStyleMessage message = 3;
+    // Fulfillment update of an action that was requested previously by
+    // the IHU SDK.
+    ActionStatusUpdate status_update = 4;
+    // Metadata of a new sender avatar icon.
+    AvatarIconSync avatar_icon_sync = 5;
+    // Request to remove all data related to a messaging application.
+    ClearAppDataRequest clear_app_data_request = 6;
+    // Informs SDK whether this feature has been enabled/disabled.
+    FeatureEnabledStateChange feature_enabled_state_change = 7;
+    // Details about the connected phone.
+    PhoneMetadata phone_metadata = 8;
+  }
+
+  // A byte array containing an undefined message. This field may contain
+  // supplemental information for a message_data, or contain all of the
+  // data for the PhoneToCarMessage.
+  bytes metadata = 9;
+}
+
+// Message to be sent from the IHU SDK to the phone SDK.
+message CarToPhoneMessage {
+  // The unique key of the message notification, same in phone and car.
+  // This will be the StatusBarNotification id of the original message
+  // notification posted on the phone.
+  string notification_key = 1;
+
+  // An action request to be fulfilled on the Phone side.
+  Action action_request = 2;
+
+  // A byte array containing an undefined message. This field may contain
+  // supplemental information for a message_data, or contain all of the
+  // data for the CarToPhoneMessage.
+  bytes metadata = 3;
+}
+
+// Message to be sent from the Phone SDK to the IHU SDK after an Action
+// has been completed. The request_id in this update will correspond to
+// the request_id of the original Action message.
+message ActionStatusUpdate {
+  // The different result types after completing an action.
+  enum Status {
+    UNKNOWN = 0;
+    SUCCESSFUL = 1;
+    ERROR = 2;
+  }
+
+  // Unique ID of the action.
+  string request_id = 1;
+
+  // The status of completing the action.
+  Status status = 2;
+
+  // Optional error message / explanation if the status resulted in an error.
+  string error_explanation = 3;
+}
+
+// A message notification originating from the user's phone.
+message ConversationNotification {
+
+  // Display name of the application that posted this notification.
+  string messaging_app_display_name = 1;
+
+  // Package name of the application that posted this notification.
+  string messaging_app_package_name = 2;
+
+  // MessagingStyle metadata of this conversation.
+  MessagingStyle messaging_style = 3;
+
+  // The time, in milliseconds, this message notification was last updated.
+  int64 time_ms = 4;
+
+  // Small app icon of the application that posted this notification.
+  bytes app_icon = 5;
+}
+
+// MessagingStyle metadata that matches MessagingStyle formatting.
+message MessagingStyle {
+  // List of messages and their metadata.
+  repeated MessagingStyleMessage messaging_style_msg = 1;
+
+  // The Conversation title of this conversation.
+  string convo_title = 2;
+
+  // String of the user, needed for MessagingStyle.
+  string user_display_name = 3;
+
+  // True if this is a group conversation.
+  bool is_group_convo = 4;
+}
+
+// Message metadata that matches MessagingStyle formatting.
+message MessagingStyleMessage {
+  // Contents of the message.
+  string text_message = 1;
+
+  // Timestamp of when the message notification was originally posted on the
+  // phone.
+  int64 timestamp = 2;
+
+  // Details of the sender who sent the message.
+  Person sender = 3;
+
+  // If the message is read on the phone.
+  bool is_read = 4;
+}
+
+// Sends over an avatar icon. This should be sent once per unique sender
+// (per unique app) during a phone to car connection.
+message AvatarIconSync {
+  // Metadata of the person.
+  Person person = 1;
+
+  // Display name of the application that posted this notification.
+  string messaging_app_display_name = 2;
+
+  // Package name of the application that posted this notification.
+  string messaging_app_package_name = 3;
+}
+
+// Request to clear all internal data and remove notifications for
+// a specific messaging application.
+message ClearAppDataRequest {
+  // Specifies which messaging app's data to remove.
+  string messaging_app_package_name = 1;
+}
+
+// Message to inform whether user has disabled/enabled this feature.
+message FeatureEnabledStateChange {
+  // Enabled state of the feature.
+  bool enabled = 1;
+}
+
+// Details of the phone that is connected to the IHU.
+message PhoneMetadata {
+  // MAC address of the phone.
+  string bluetooth_device_address = 1;
+}
+
+// Metadata about a sender.
+message Person {
+  // Sender's name.
+  string name = 1;
+
+  // Sender's avatar icon.
+  bytes avatar = 2;
+
+  // Sender's low-resolution thumbnail
+  bytes thumbnail = 3;
+}
+
+// Action on a notification, initiated by the user on the IHU.
+message Action {
+  // Different types of actions user can do on the IHU notification.
+  enum ActionName {
+    UNKNOWN_ACTION_NAME = 0;
+    MARK_AS_READ = 1;
+    REPLY = 2;
+    DISMISS = 3;
+  }
+
+  // Same as the PhoneToCar and CarToPhone messages's notification_key.
+  // As mentioned above, this notification id should be the same on the
+  // phone and the car. This will be the StatusBarNotification id of the
+  // original message notification posted on the phone.
+  string notification_key = 1;
+
+  //Optional, used to capture data like the reply string.
+  repeated MapEntry map_entry = 2;
+
+  // Name of the action.
+  ActionName action_name = 3;
+
+  // Unique id of this action.
+  string request_id = 4;
+}
+
+// Backwards compatible way of supporting a map.
+message MapEntry {
+  // Key for the map.
+  string key = 1;
+
+  // Value that is mapped to this key.
+  string value = 2;
+}
diff --git a/car-messenger-common/res/drawable/ic_message.xml b/car-messenger-common/res/drawable/ic_message.xml
new file mode 100644
index 0000000..fd6026f
--- /dev/null
+++ b/car-messenger-common/res/drawable/ic_message.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
+    android:viewportWidth="48"
+    android:viewportHeight="48"
+    android:width="48dp"
+    android:height="48dp">
+  <path
+      android:pathData="M40 4H8C5.79 4 4.02 5.79 4.02 8L4 44l8 -8h28c2.21 0 4 -1.79 4 -4V8c0 -2.21 -1.79 -4 -4 -4zM12 18h24v4H12v-4zm16 10H12v-4h16v4zm8 -12H12v-4h24v4z"
+      android:fillColor="#FFFFFF" />
+</vector>
diff --git a/car-messenger-common/res/values-af/strings.xml b/car-messenger-common/res/values-af/strings.xml
new file mode 100644
index 0000000..7ffff1f
--- /dev/null
+++ b/car-messenger-common/res/values-af/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d nuwe boodskappe</item>
+      <item quantity="one">Nuwe boodskap</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Speel"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Merk as gelees"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Herhaal"</string>
+    <string name="action_reply" msgid="564106590567600685">"Antwoord"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stop"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Maak toe"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Kan nie antwoord stuur nie Probeer weer."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Kan nie antwoord stuur nie Toestel is nie gekoppel nie."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s sê"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Kan nie boodskap hardop lees nie."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Antwoord is gestuur na %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Naam is nie beskikbaar nie"</string>
+</resources>
diff --git a/car-messenger-common/res/values-am/strings.xml b/car-messenger-common/res/values-am/strings.xml
new file mode 100644
index 0000000..8d11d19
--- /dev/null
+++ b/car-messenger-common/res/values-am/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d አዲስ መልዕክቶች</item>
+      <item quantity="other">%d አዲስ መልዕክቶች</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"አጫውት"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"እንደተነበበ ምልክት አድርግ"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"ድገም"</string>
+    <string name="action_reply" msgid="564106590567600685">"ምላሽ ስጥ"</string>
+    <string name="action_stop" msgid="6950369080845695405">"አስቁም"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"ዝጋ"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"ምላሽ መላክ አልተቻለም። እባክዎ እንደገና ይሞክሩ።"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"ምላሽ መላክ አልተቻለም። መሣሪያ አልተገናኘም።"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s እንዲህ ይላሉ፦"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"መልዕክቱን ማንበብ አልተቻለም።"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"«%s»"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"ምላሽ ለ%s ተልኳል"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"ስም አይገኝም"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ar/strings.xml b/car-messenger-common/res/values-ar/strings.xml
new file mode 100644
index 0000000..c2b2eb2
--- /dev/null
+++ b/car-messenger-common/res/values-ar/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="zero">%d رسالة جديدة.</item>
+      <item quantity="two">رسالتان جديدتان (%d)</item>
+      <item quantity="few">%d رسائل جديدة</item>
+      <item quantity="many">%d رسالة جديدة</item>
+      <item quantity="other">%d رسالة جديدة</item>
+      <item quantity="one">رسالة واحدة جديدة</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"تشغيل"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"وضع علامة \"مقروءة\""</string>
+    <string name="action_repeat" msgid="8184323082093728957">"تكرار"</string>
+    <string name="action_reply" msgid="564106590567600685">"رد"</string>
+    <string name="action_stop" msgid="6950369080845695405">"إيقاف"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"إغلاق"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"تعذّر إرسال رد. يُرجى إعادة المحاولة."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"تعذّر إرسال رد. الجهاز غير متصل."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"رسالة %s نصها"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"لا يمكن قراءة الرسالة بصوت عالٍ."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"تم إرسال رد إلى %s."</string>
+    <string name="name_not_available" msgid="3800013092212550915">"الاسم غير متاح."</string>
+</resources>
diff --git a/car-messenger-common/res/values-as/strings.xml b/car-messenger-common/res/values-as/strings.xml
new file mode 100644
index 0000000..bbf92d1
--- /dev/null
+++ b/car-messenger-common/res/values-as/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%dটা নতুন বাৰ্তা</item>
+      <item quantity="other">%dটা নতুন বাৰ্তা</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"প্লে’ কৰক"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"পঢ়া হৈছে বুলি চিহ্নিত কৰক"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"পুনৰাই কৰক"</string>
+    <string name="action_reply" msgid="564106590567600685">"প্ৰত্যুত্তৰ দিয়ক"</string>
+    <string name="action_stop" msgid="6950369080845695405">"বন্ধ কৰক"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"বন্ধ কৰক"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"প্ৰত্যুত্তৰ পঠিয়াব নোৱাৰি। অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক।"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"প্ৰত্যুত্তৰ পঠিয়াব নোৱাৰি। ডিভাইচটো সংযুক্ত হৈ নাই।"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%sএ কৈছে"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"বাৰ্তাটো পঢ়িব নোৱাৰি।"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%sলৈ প্রত্যুত্তৰ পঠিওৱা হ’ল"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"নাম উপলব্ধ নহয়"</string>
+</resources>
diff --git a/car-messenger-common/res/values-az/strings.xml b/car-messenger-common/res/values-az/strings.xml
new file mode 100644
index 0000000..b93f53c
--- /dev/null
+++ b/car-messenger-common/res/values-az/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d yeni mesaj</item>
+      <item quantity="one">Yeni mesaj</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Oxudun"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Oxunmuş kimi qeyd edin"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Təkrarlayın"</string>
+    <string name="action_reply" msgid="564106590567600685">"Cavablayın"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Dayandırın"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Bağlayın"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Cavab göndərmək alınmadı. Yenidən cəhd edin."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Cavab göndərmək alınmadı. Cihaz qoşulmayıb."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s söyləyir:"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Mesajı oxumaq mümkün deyil."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Cavab bu ünvana göndərildi: %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Ad əlçatan deyil"</string>
+</resources>
diff --git a/car-messenger-common/res/values-b+sr+Latn/strings.xml b/car-messenger-common/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..8421afb
--- /dev/null
+++ b/car-messenger-common/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d nova poruka</item>
+      <item quantity="few">%d nove poruke</item>
+      <item quantity="other">%d novih poruka</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Pusti"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Označi kao pročitano"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Ponovi"</string>
+    <string name="action_reply" msgid="564106590567600685">"Odgovori"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Zaustavi"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Zatvori"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Slanje odgovora nije uspelo. Probajte ponovo."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Slanje odgovora nije uspelo. Uređaj nije povezan."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s kaže"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Čitanje poruke naglas nije uspelo."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s“"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Odgovor je poslat kontaktu %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Ime nije dostupno"</string>
+</resources>
diff --git a/car-messenger-common/res/values-be/strings.xml b/car-messenger-common/res/values-be/strings.xml
new file mode 100644
index 0000000..fa6e154
--- /dev/null
+++ b/car-messenger-common/res/values-be/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d новае паведамленне</item>
+      <item quantity="few">%d новыя паведамленні</item>
+      <item quantity="many">%d новых паведамленняў</item>
+      <item quantity="other">%d новага паведамлення</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Прайграць"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Пазначыць як прачытанае"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Паўтараць"</string>
+    <string name="action_reply" msgid="564106590567600685">"Адказаць"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Спыніць"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Закрыць"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Не ўдалося адправіць адказ. Паўтарыце спробу."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Не ўдалося адправіць адказ. Прылада не падключана."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s кажа"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Не ўдалося зачытаць паведамленне."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Адказ адпраўлены кантакту %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Імя недаступнае"</string>
+</resources>
diff --git a/car-messenger-common/res/values-bg/strings.xml b/car-messenger-common/res/values-bg/strings.xml
new file mode 100644
index 0000000..911ea86
--- /dev/null
+++ b/car-messenger-common/res/values-bg/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d нови съобщения</item>
+      <item quantity="one">Ново съобщение</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Възпроизвеждане"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Означаване като прочетено"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Повтаряне"</string>
+    <string name="action_reply" msgid="564106590567600685">"Отговор"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Спиране"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Затваряне"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Изпращането на отговора не бе успешно. Моля, опитайте отново."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Изпращането на отговора не бе успешно. Устройството не е свързано."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s казва"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Съобщението не може да бъде прочетено."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s“"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Отговорът бе изпратен до %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Няма име"</string>
+</resources>
diff --git a/car-messenger-common/res/values-bn/strings.xml b/car-messenger-common/res/values-bn/strings.xml
new file mode 100644
index 0000000..bf17122
--- /dev/null
+++ b/car-messenger-common/res/values-bn/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%dটি নতুন মেসেজ</item>
+      <item quantity="other">%dটি নতুন মেসেজ</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"চালান"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"\'পড়া হয়েছে\' হিসেবে চিহ্নিত করুন"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"পুনরাবৃত্তি করুন"</string>
+    <string name="action_reply" msgid="564106590567600685">"উত্তর"</string>
+    <string name="action_stop" msgid="6950369080845695405">"থামান"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"বন্ধ করুন"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"উত্তর পাঠানো যায়নি। আবার চেষ্টা করুন।"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"উত্তর পাঠানো যায়নি। ডিভাইস কানেক্ট করা নেই।"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s বলছে"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"মেসেজ পড়া যাচ্ছে না।"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s-এ উত্তর পাঠানো হয়েছে"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"নাম উপলভ্য নেই"</string>
+</resources>
diff --git a/car-messenger-common/res/values-bs/strings.xml b/car-messenger-common/res/values-bs/strings.xml
new file mode 100644
index 0000000..544296a
--- /dev/null
+++ b/car-messenger-common/res/values-bs/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d nova poruka</item>
+      <item quantity="few">%d nove poruke</item>
+      <item quantity="other">%d novih poruka</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Reproduciraj"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Označi kao pročitano"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Ponovi"</string>
+    <string name="action_reply" msgid="564106590567600685">"Odgovori"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Zaustavi"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Zatvori"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Nije moguće poslati odgovor. Pokušajte ponovo."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Nije moguće poslati odgovor. Uređaj nije povezan."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s kaže"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Nije moguće pročitati poruku naglas."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Odgovor je poslan kontaktu %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Ime nije dostupno"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ca/strings.xml b/car-messenger-common/res/values-ca/strings.xml
new file mode 100644
index 0000000..09174e7
--- /dev/null
+++ b/car-messenger-common/res/values-ca/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d missatges nous</item>
+      <item quantity="one">Missatge nou</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Reprodueix"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Marca com a llegit"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repeteix"</string>
+    <string name="action_reply" msgid="564106590567600685">"Respon"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Atura"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Tanca"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"No s\'ha pogut enviar la resposta. Torna-ho a provar."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"No s\'ha pogut enviar la resposta. El dispositiu no està connectat."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s diu"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"No es pot llegir el missatge en veu alta."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"S\'ha enviat la resposta a %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nom no disponible"</string>
+</resources>
diff --git a/car-messenger-common/res/values-cs/strings.xml b/car-messenger-common/res/values-cs/strings.xml
new file mode 100644
index 0000000..bd4c28a
--- /dev/null
+++ b/car-messenger-common/res/values-cs/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="few">%d nové zprávy</item>
+      <item quantity="many">%d nové zprávy</item>
+      <item quantity="other">%d nových zpráv</item>
+      <item quantity="one">Nová zpráva</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Přehrát"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Označit jako přečtené"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Opakování"</string>
+    <string name="action_reply" msgid="564106590567600685">"Odpovědět"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Zastavit"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Zavřít"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Odpověď se nepodařilo odeslat. Zkuste to znovu."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Odpověď se nepodařilo odeslat. Zařízení není připojeno."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s říká"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Zprávu se nepodařilo přečíst."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s“"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Odpověď odeslána příjemci %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Jméno není dostupné"</string>
+</resources>
diff --git a/car-messenger-common/res/values-da/strings.xml b/car-messenger-common/res/values-da/strings.xml
new file mode 100644
index 0000000..f18e544
--- /dev/null
+++ b/car-messenger-common/res/values-da/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d ny besked</item>
+      <item quantity="other">%d nye beskeder</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Afspil"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Markér som læst"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Gentag"</string>
+    <string name="action_reply" msgid="564106590567600685">"Svar"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stop"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Luk"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Svaret kunne ikke sendes. Prøv igen."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Svaret kunne ikke sendes. Enheden er ikke tilsluttet."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s siger"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Beskeden kan ikke læses højt."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Svaret er sendt til %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Navnet er ikke tilgængeligt"</string>
+</resources>
diff --git a/car-messenger-common/res/values-de/strings.xml b/car-messenger-common/res/values-de/strings.xml
new file mode 100644
index 0000000..274b1f1
--- /dev/null
+++ b/car-messenger-common/res/values-de/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d neue Nachrichten</item>
+      <item quantity="one">Neue Nachricht</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Wiedergeben"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Als gelesen markieren"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Wiederholen"</string>
+    <string name="action_reply" msgid="564106590567600685">"Antworten"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Abbrechen"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Schließen"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Antwort kann nicht gesendet werden. Bitte versuch es noch einmal."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Antwort kann nicht gesendet werden. Gerät ist nicht verbunden."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s sagt"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Die Nachricht kann nicht vorgelesen werden."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Antwort wurde an %s gesendet"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Name nicht verfügbar"</string>
+</resources>
diff --git a/car-messenger-common/res/values-el/strings.xml b/car-messenger-common/res/values-el/strings.xml
new file mode 100644
index 0000000..1429ba4
--- /dev/null
+++ b/car-messenger-common/res/values-el/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d νέα μηνύματα</item>
+      <item quantity="one">Νέο μήνυμα</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Αναπαραγωγή"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Επισήμανση ως αναγνωσμένο"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Επανάληψη"</string>
+    <string name="action_reply" msgid="564106590567600685">"Απάντηση"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Διακοπή"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Κλείσιμο"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Δεν είναι δυνατή η αποστολή απάντησης. Δοκιμάστε ξανά."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Δεν είναι δυνατή η αποστολή απάντησης. Η συσκευή δεν είναι συνδεδεμένη."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"Ο χρήστης %s λέει"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Δεν είναι δυνατή η ανάγνωση του μηνύματος."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"%s"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Η απάντηση στάλθηκε στον χρήστη %s."</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Το όνομα δεν είναι διαθέσιμο."</string>
+</resources>
diff --git a/car-messenger-common/res/values-en-rAU/strings.xml b/car-messenger-common/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..e50266e
--- /dev/null
+++ b/car-messenger-common/res/values-en-rAU/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d new messages</item>
+      <item quantity="one">New message</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Play"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Mark as read"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repeat"</string>
+    <string name="action_reply" msgid="564106590567600685">"Reply"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stop"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Close"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Unable to send reply. Please try again."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Unable to send reply. Device is not connected."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s says"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Can\'t read out message."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\'%s\'"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Reply sent to %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Name not available"</string>
+</resources>
diff --git a/car-messenger-common/res/values-en-rCA/strings.xml b/car-messenger-common/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..e50266e
--- /dev/null
+++ b/car-messenger-common/res/values-en-rCA/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d new messages</item>
+      <item quantity="one">New message</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Play"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Mark as read"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repeat"</string>
+    <string name="action_reply" msgid="564106590567600685">"Reply"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stop"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Close"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Unable to send reply. Please try again."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Unable to send reply. Device is not connected."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s says"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Can\'t read out message."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\'%s\'"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Reply sent to %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Name not available"</string>
+</resources>
diff --git a/car-messenger-common/res/values-en-rGB/strings.xml b/car-messenger-common/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..e50266e
--- /dev/null
+++ b/car-messenger-common/res/values-en-rGB/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d new messages</item>
+      <item quantity="one">New message</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Play"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Mark as read"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repeat"</string>
+    <string name="action_reply" msgid="564106590567600685">"Reply"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stop"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Close"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Unable to send reply. Please try again."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Unable to send reply. Device is not connected."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s says"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Can\'t read out message."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\'%s\'"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Reply sent to %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Name not available"</string>
+</resources>
diff --git a/car-messenger-common/res/values-en-rIN/strings.xml b/car-messenger-common/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..e50266e
--- /dev/null
+++ b/car-messenger-common/res/values-en-rIN/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d new messages</item>
+      <item quantity="one">New message</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Play"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Mark as read"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repeat"</string>
+    <string name="action_reply" msgid="564106590567600685">"Reply"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stop"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Close"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Unable to send reply. Please try again."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Unable to send reply. Device is not connected."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s says"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Can\'t read out message."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\'%s\'"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Reply sent to %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Name not available"</string>
+</resources>
diff --git a/car-messenger-common/res/values-en-rXC/strings.xml b/car-messenger-common/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..99a3fb8
--- /dev/null
+++ b/car-messenger-common/res/values-en-rXC/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‎‏‏‎‏‏‎‏‎‏‎‎‎‏‏‏‎‏‏‎‎‏‎‏‎‏‎‎‏‏‏‏‎‏‏‏‏‏‎‎‎‎‏‎‏‎‎‏‏‎‏‏‎‏‎‎‎‏‎‏‎‎‏‎%d new messages‎‏‎‎‏‎</item>
+      <item quantity="one">‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‎‏‏‎‏‏‎‏‎‏‎‎‎‏‏‏‎‏‏‎‎‏‎‏‎‏‎‎‏‏‏‏‎‏‏‏‏‏‎‎‎‎‏‎‏‎‎‏‏‎‏‏‎‏‎‎‎‏‎‏‎‎‏‎New message‎‏‎‎‏‎</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‎‏‏‏‎‏‎‎‎‏‎‎‏‏‏‎‏‏‎‎‎‎‎‎‎‎‎‎‏‏‎‏‎‎‏‏‏‏‏‎‎‏‎‎‏‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‏‏‎‎Play‎‏‎‎‏‎"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‎‏‏‏‏‏‏‏‎‏‎‏‏‎‎‏‎‏‏‏‎‏‎‏‎‏‎‏‏‎‎‏‏‏‎‎‎‏‏‏‏‏‎‎‎‏‏‎‏‎‏‎‏‎‎‎‎‎‏‎‎Mark As Read‎‏‎‎‏‎"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‎‎‏‎‏‎‎‏‎‎‎‏‏‏‎‎‏‏‏‏‎‏‏‎‎‎‏‎‎‏‏‎‎‏‏‏‎‏‎‎‎‎‏‎‏‎‎‏‎‏‏‏‏‎‏‎Repeat‎‏‎‎‏‎"</string>
+    <string name="action_reply" msgid="564106590567600685">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‎‏‏‏‏‏‏‏‎‏‎‏‎‎‎‎‎‏‏‎‏‏‏‏‏‏‎‏‎‎‏‎‎‏‎‎‏‎‎‎‏‏‏‏‎‏‎‎‎‏‎‎‏‎‎‎‏‎‏‏‎‏‎Reply‎‏‎‎‏‎"</string>
+    <string name="action_stop" msgid="6950369080845695405">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‎‎‎‏‏‏‎‏‎‎‏‎‏‎‏‎‏‏‏‏‎‏‏‎‏‎‏‎‏‏‏‎‎‏‏‎‏‎‏‎‏‎‎‏‏‏‏‎‎‏‏‎‏‎‏‏‎‏‎Stop‎‏‎‎‏‎"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‎‏‎‎‎‏‏‎‎‏‎‎‏‎‏‎‎‏‎‎‎‎‎‎‏‎‎‎‎‏‎‎‎‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‎‎‏‎‎‎‎Close‎‏‎‎‏‎"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‎‏‏‏‎‏‎‎‏‎‏‏‏‏‎‏‎‎‏‎‏‏‎‏‏‏‏‎‏‏‏‏‎‎‏‎‎‏‏‎‎‏‎‎‏‏‏‏‎‎‎‏‏‎‏‏‎Unable to send reply. Please try again.‎‏‎‎‏‎"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‎‎‎‏‎‏‎‏‏‎‎‏‎‎‏‏‎‎‏‏‎‏‎‎‎‎‏‏‏‎‎‏‎‎‎‎‎‏‏‎‏‎‏‏‏‏‏‏‎‎‎‏‏‎‏‏‏‏‏‎‎Unable to send reply. Device is not connected.‎‏‎‎‏‎"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‏‎‏‎‎‏‎‎‏‎‎‎‏‎‎‏‏‎‏‎‎‏‏‏‏‎‏‎‎‎‏‎‎‏‏‎‎‎‎‏‎‎‏‏‏‏‏‎‎‎‏‏‎‎‏‏‎‏‎‎‎%s says‎‏‎‎‏‎"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‎‏‏‎‏‎‎‏‎‎‏‎‏‎‏‏‏‎‎‏‎‎‏‏‏‎‏‎‎‎‎‏‏‎‎‏‎‏‏‏‎‎‏‎‎‎‎‏‎‏‏‏‎‎‎‏‏‎‏‎‎‎‏‎Can\'t read out message.‎‏‎‎‏‎"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‎‏‏‎‏‎‏‏‎‏‎‏‏‎‏‏‏‎‏‎‏‎‎‎‏‏‏‎‏‎‏‏‏‏‏‎‏‎‏‏‏‎‎‎‎‏‎‏‎‏‏‏‏‎‎\"%s\"‎‏‎‎‏‎"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‎‎‏‎‏‎‎‎‏‎‏‎‏‎‏‎‎‏‏‎‎‏‏‏‎‎‎‏‎‏‎‎‎‎‏‏‎‏‎‏‎‏‎‏‏‎‎‏‎‎‎‎‎‏‎Reply sent to %s‎‏‎‎‏‎"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‏‏‏‎‏‏‎‎‎‏‎‏‏‏‏‏‎‏‏‎‏‎‎‏‎‏‏‏‏‎‎‎‏‎‏‏‎‏‏‏‏‎‎‎‏‏‎‎‎‏‎‎‏‏‏‎‎‏‎‏‎‎‏‏‎‏‎‏‏‎‏‎‎‎‎‎‎‏‏‎Name not available‎‏‎‎‏‎"</string>
+</resources>
diff --git a/car-messenger-common/res/values-es-rUS/strings.xml b/car-messenger-common/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..bfd0c97
--- /dev/null
+++ b/car-messenger-common/res/values-es-rUS/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d mensajes nuevos</item>
+      <item quantity="one">Mensaje nuevo</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Reproducir"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Marcar como leído"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repetir"</string>
+    <string name="action_reply" msgid="564106590567600685">"Responder"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Detener"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Cerrar"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"No se pudo enviar la respuesta. Vuelve a intentarlo."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"No se pudo enviar la respuesta. El dispositivo no está conectado."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s dice"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"No se puede leer en voz alta el mensaje."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Se envió la respuesta a %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nombre no disponible"</string>
+</resources>
diff --git a/car-messenger-common/res/values-es/strings.xml b/car-messenger-common/res/values-es/strings.xml
new file mode 100644
index 0000000..0a66752
--- /dev/null
+++ b/car-messenger-common/res/values-es/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d mensajes nuevos</item>
+      <item quantity="one">Mensaje nuevo</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Reproducir"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Marcar como leído"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repetir"</string>
+    <string name="action_reply" msgid="564106590567600685">"Responder"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Detener"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Cerrar"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"No se ha podido enviar la respuesta. Inténtalo de nuevo."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"No se ha podido enviar la respuesta. El dispositivo no está conectado."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s dice"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"No se puede leer el mensaje en voz alta."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Se ha enviado la respuesta a %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"El nombre no está disponible"</string>
+</resources>
diff --git a/car-messenger-common/res/values-et/strings.xml b/car-messenger-common/res/values-et/strings.xml
new file mode 100644
index 0000000..e4167f6
--- /dev/null
+++ b/car-messenger-common/res/values-et/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d uut sõnumit</item>
+      <item quantity="one">Uus sõnum</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Esita"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Märgi loetuks"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Korda"</string>
+    <string name="action_reply" msgid="564106590567600685">"Vasta"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Peata"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Sule"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Vastust ei saa saata. Proovige uuesti."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Vastust ei saa saata. Seade pole ühendatud."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s ütleb"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Sõnumit ei saa ette lugeda."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s”"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Vastus saadeti üksusele %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nimi pole saadaval"</string>
+</resources>
diff --git a/car-messenger-common/res/values-eu/strings.xml b/car-messenger-common/res/values-eu/strings.xml
new file mode 100644
index 0000000..7a5c20e
--- /dev/null
+++ b/car-messenger-common/res/values-eu/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d mezu berri</item>
+      <item quantity="one">Mezu berria</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Erreproduzitu"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Markatu irakurritako gisa"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Errepikatu"</string>
+    <string name="action_reply" msgid="564106590567600685">"Erantzun"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Gelditu"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Itxi"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Ezin da bidali erantzuna. Saiatu berriro."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Ezin da bidali erantzuna. Gailua ez dago konektatuta."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s erabiltzaileak hau dio:"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Ezin da irakurri ozen mezua."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Erantzuna bidali zaio %s erabiltzaileari"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Izena ez dago erabilgarri"</string>
+</resources>
diff --git a/car-messenger-common/res/values-fa/strings.xml b/car-messenger-common/res/values-fa/strings.xml
new file mode 100644
index 0000000..71fc229
--- /dev/null
+++ b/car-messenger-common/res/values-fa/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d پیام جدید</item>
+      <item quantity="other">%d پیام جدید</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"پخش"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"علامت‌گذاری به‌عنوان خوانده‌شده"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"تکرار"</string>
+    <string name="action_reply" msgid="564106590567600685">"پاسخ"</string>
+    <string name="action_stop" msgid="6950369080845695405">"توقف"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"بستن"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"پاسخ ارسال نشد. لطفاً دوباره امتحان کنید."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"پاسخ ارسال نشد. دستگاه متصل نشده است."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s می‌گوید"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"خواندن پیام ممکن نیست."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"«%s»"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"پاسخ برای %s ارسال شد"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"نام در دسترس نیست"</string>
+</resources>
diff --git a/car-messenger-common/res/values-fi/strings.xml b/car-messenger-common/res/values-fi/strings.xml
new file mode 100644
index 0000000..4e905c2
--- /dev/null
+++ b/car-messenger-common/res/values-fi/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d uutta viestiä</item>
+      <item quantity="one">Uusi viesti</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Toista"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Merkitse luetuksi"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Toista uudelleen"</string>
+    <string name="action_reply" msgid="564106590567600685">"Vastaa"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Lopeta"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Sulje"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Vastaaminen ei onnistunut. Yritä uudelleen."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Vastaaminen ei onnistunut. Laite ei saa yhteyttä."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s sanoo"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Viestiä ei voi lukea."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Vastaus lähetetty: %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nimi ei ole saatavilla"</string>
+</resources>
diff --git a/car-messenger-common/res/values-fr-rCA/strings.xml b/car-messenger-common/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..56086e6
--- /dev/null
+++ b/car-messenger-common/res/values-fr-rCA/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d nouveau message</item>
+      <item quantity="other">%d nouveaux messages</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Faire jouer"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Marquer comme lu"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Répéter"</string>
+    <string name="action_reply" msgid="564106590567600685">"Répondre"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Arrêter"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Fermer"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Impossible d\'envoyer la réponse. Veuillez réessayer."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Impossible d\'envoyer la réponse. L\'appareil n\'est pas connecté."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s dit"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Impossible de lire le message."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"« %s »"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Réponse envoyée à %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nom indisponible"</string>
+</resources>
diff --git a/car-messenger-common/res/values-fr/strings.xml b/car-messenger-common/res/values-fr/strings.xml
new file mode 100644
index 0000000..a96b14f
--- /dev/null
+++ b/car-messenger-common/res/values-fr/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d nouveau message</item>
+      <item quantity="other">%d nouveaux messages</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Lire"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Marquer comme lu"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Répéter"</string>
+    <string name="action_reply" msgid="564106590567600685">"Répondre"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Arrêter"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Fermer"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Impossible d\'envoyer la réponse. Veuillez réessayer."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Impossible d\'envoyer la réponse. L\'appareil n\'est pas connecté."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s dit"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Impossible de lire le message à haute voix."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Réponse envoyée à %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nom indisponible"</string>
+</resources>
diff --git a/car-messenger-common/res/values-gl/strings.xml b/car-messenger-common/res/values-gl/strings.xml
new file mode 100644
index 0000000..b81c8e7
--- /dev/null
+++ b/car-messenger-common/res/values-gl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d mensaxes novas</item>
+      <item quantity="one">Mensaxe nova</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Reproducir"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Marcar como lido"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repetir"</string>
+    <string name="action_reply" msgid="564106590567600685">"Responder"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Deter"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Pechar"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Non se puido enviar a resposta. Téntao de novo."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Non se puido enviar a resposta. O dispositivo non está conectado."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s di"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Non se puido ler a mensaxe."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Enviouse a resposta a %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"O nome non está dispoñible"</string>
+</resources>
diff --git a/car-messenger-common/res/values-gu/strings.xml b/car-messenger-common/res/values-gu/strings.xml
new file mode 100644
index 0000000..3871966
--- /dev/null
+++ b/car-messenger-common/res/values-gu/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d નવો સંદેશ</item>
+      <item quantity="other">%d નવા સંદેશા</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"ચલાવો"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"વાંચેલાં તરીકે ચિહ્નિત કરો"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"પુનરાવર્તન"</string>
+    <string name="action_reply" msgid="564106590567600685">"જવાબ આપો"</string>
+    <string name="action_stop" msgid="6950369080845695405">"રોકો"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"બંધ કરો"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"જવાબ મોકલી શકતા નથી. કૃપા કરી ફરી પ્રયાસ કરો."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"જવાબ મોકલી શકતા નથી. ડિવાઇસ કનેક્ટ થયું નથી."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s કહે છે"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"સંદેશ વાંચી શકાતો નથી."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s પર જવાબ મોકલ્યો છે"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"નામ ઉપલબ્ધ નથી"</string>
+</resources>
diff --git a/car-messenger-common/res/values-hi/strings.xml b/car-messenger-common/res/values-hi/strings.xml
new file mode 100644
index 0000000..74a5693
--- /dev/null
+++ b/car-messenger-common/res/values-hi/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d नए मैसेज</item>
+      <item quantity="other">%d नए मैसेज</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"चलाएं"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"\'पढ़ा गया\' का निशान लगाएं"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"दोहराएं"</string>
+    <string name="action_reply" msgid="564106590567600685">"जवाब दें"</string>
+    <string name="action_stop" msgid="6950369080845695405">"रोकें"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"बंद करें"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"जवाब नहीं भेजा जा सका. कृपया फिर से कोशिश करें."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"जवाब नहीं भेजा जा सका. डिवाइस कनेक्ट नहीं है."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s का मैसेज यह है"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"मैसेज पढ़ा नहीं जा सकता."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s को जवाब भेजा गया"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"नाम उपलब्ध नहीं है"</string>
+</resources>
diff --git a/car-messenger-common/res/values-hr/strings.xml b/car-messenger-common/res/values-hr/strings.xml
new file mode 100644
index 0000000..8a46b5d
--- /dev/null
+++ b/car-messenger-common/res/values-hr/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d nova poruka</item>
+      <item quantity="few">%d nove poruke</item>
+      <item quantity="other">%d novih poruka</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Pokreni"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Označi kao pročitano"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Ponovi"</string>
+    <string name="action_reply" msgid="564106590567600685">"Odgovori"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Zaustavi"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Zatvori"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Slanje odgovora nije uspjelo. Pokušajte ponovo."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Slanje odgovora nije uspjelo. Uređaj nije povezan."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s kaže"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Nije moguće pročitati poruku naglas."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Odgovor je poslan kontaktu %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Ime nije dostupno"</string>
+</resources>
diff --git a/car-messenger-common/res/values-hu/strings.xml b/car-messenger-common/res/values-hu/strings.xml
new file mode 100644
index 0000000..c6d7693
--- /dev/null
+++ b/car-messenger-common/res/values-hu/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d új üzenet</item>
+      <item quantity="one">Új üzenet</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Lejátszás"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Megjelölés olvasottként"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Ismétlés"</string>
+    <string name="action_reply" msgid="564106590567600685">"Válasz"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Leállítás"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Bezárás"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Nem sikerült a válasz elküldése. Próbálja újra."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Nem sikerült a válasz elküldése. Az eszköz nincs csatlakoztatva."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s a következőt küldte:"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Az üzenet felolvasása nem sikerült."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s”"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Válasz elküldve a következőnek: %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"A név nem használható"</string>
+</resources>
diff --git a/car-messenger-common/res/values-hy/strings.xml b/car-messenger-common/res/values-hy/strings.xml
new file mode 100644
index 0000000..c6faa75
--- /dev/null
+++ b/car-messenger-common/res/values-hy/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d նոր հաղորդագրություն</item>
+      <item quantity="other">%d նոր հաղորդագրություն</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Նվագարկել"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Նշել որպես կարդացված"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Կրկնել"</string>
+    <string name="action_reply" msgid="564106590567600685">"Պատասխանել"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Դադարեցնել"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Փակել"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Չհաջողվեց ուղարկել պատասխանը։ Նորից փորձեք:"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Չհաջողվեց ուղարկել պատասխանը։ Սարքը միացված չէ։"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"Հաղորդագրություն %s-ից․"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Չհաջողվեց ընթերցել հաղորդագրությունը։"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"«%s»"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Պատասխանն ուղարկվեց %s-ին"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Անունը հասանելի չէ"</string>
+</resources>
diff --git a/car-messenger-common/res/values-in/strings.xml b/car-messenger-common/res/values-in/strings.xml
new file mode 100644
index 0000000..12182d4
--- /dev/null
+++ b/car-messenger-common/res/values-in/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d pesan baru</item>
+      <item quantity="one">Pesan baru</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Putar"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Tandai Telah Dibaca"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Ulangi"</string>
+    <string name="action_reply" msgid="564106590567600685">"Balas"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Berhenti"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Tutup"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Tidak dapat mengirim balasan. Coba lagi."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Tidak dapat mengirim balasan. Perangkat tidak terhubung"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s mengatakan"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Tidak dapat membacakan pesan."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Jawaban dikirim ke %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nama tidak tersedia"</string>
+</resources>
diff --git a/car-messenger-common/res/values-is/strings.xml b/car-messenger-common/res/values-is/strings.xml
new file mode 100644
index 0000000..079b6d5
--- /dev/null
+++ b/car-messenger-common/res/values-is/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d ný skilaboð</item>
+      <item quantity="other">%d ný skilaboð</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Spila"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Merkja sem lesið"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Endurtaka"</string>
+    <string name="action_reply" msgid="564106590567600685">"Svara"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stöðva"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Loka"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Ekki tókst að senda svar. Reyndu aftur."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Ekki tókst að senda svar. Tækið er ekki tengt."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s segir"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Ekki er hægt að lesa upp skilaboð."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s“"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Svar sent til %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nafnið er ekki tiltækt"</string>
+</resources>
diff --git a/car-messenger-common/res/values-it/strings.xml b/car-messenger-common/res/values-it/strings.xml
new file mode 100644
index 0000000..a1f750d
--- /dev/null
+++ b/car-messenger-common/res/values-it/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d nuovi messaggi</item>
+      <item quantity="one">Nuovo messaggio</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Riproduci"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Segna come letto"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Ripeti"</string>
+    <string name="action_reply" msgid="564106590567600685">"Rispondi"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Interrompi"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Chiudi"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Impossibile inviare la risposta. Riprova."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Impossibile inviare la risposta. Il dispositivo non è collegato."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s dice"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Impossibile leggere il messaggio ad alta voce."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Risposta inviata a %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nome non disponibile"</string>
+</resources>
diff --git a/car-messenger-common/res/values-iw/strings.xml b/car-messenger-common/res/values-iw/strings.xml
new file mode 100644
index 0000000..c28823d
--- /dev/null
+++ b/car-messenger-common/res/values-iw/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="two">‎%d‎ הודעות חדשות</item>
+      <item quantity="many">‎%d‎ הודעות חדשות</item>
+      <item quantity="other">‎%d‎ הודעות חדשות</item>
+      <item quantity="one">הודעה חדשה</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"הפעלה"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"סימון כפריט שנקרא"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"חזרה"</string>
+    <string name="action_reply" msgid="564106590567600685">"שליחת תשובה"</string>
+    <string name="action_stop" msgid="6950369080845695405">"עצירה"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"סגירה"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"לא ניתן לשלוח תשובה. יש לנסות שוב."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"לא ניתן לשלוח תשובה. המכשיר לא מחובר."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s אומר/ת"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"לא ניתן להקריא את ההודעה."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"התשובה נשלחה אל %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"השם לא זמין"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ja/strings.xml b/car-messenger-common/res/values-ja/strings.xml
new file mode 100644
index 0000000..6393b4f
--- /dev/null
+++ b/car-messenger-common/res/values-ja/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d 件の新着メッセージ</item>
+      <item quantity="one">新着メッセージ</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"再生"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"既読にする"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"繰り返し"</string>
+    <string name="action_reply" msgid="564106590567600685">"返信"</string>
+    <string name="action_stop" msgid="6950369080845695405">"停止"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"閉じる"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"返信できませんでした。もう一度お試しください。"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"返信できませんでした。デバイスが接続されていません。"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s さんからのメッセージです"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"メッセージを読み上げられません。"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"「%s」"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s さんに返信しました"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"名前がありません"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ka/strings.xml b/car-messenger-common/res/values-ka/strings.xml
new file mode 100644
index 0000000..29c46f3
--- /dev/null
+++ b/car-messenger-common/res/values-ka/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d ახალი შეტყობინება</item>
+      <item quantity="one">ახალი შეტყობინება</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"დაკვრა"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"წაკითხულად მონიშვნა"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"გამეორება"</string>
+    <string name="action_reply" msgid="564106590567600685">"პასუხი"</string>
+    <string name="action_stop" msgid="6950369080845695405">"შეწყვეტა"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"დახურვა"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"პასუხის გაგზავნა ვერ მოხერხდა. გთხოვთ, ცადოთ ხელახლა."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"პასუხის გაგზავნა ვერ მოხერხდა. მოწყობილობა დაკავშირებული არ არის."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s ამბობს"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"შეტყობინების ხმამაღლა წაკითხვა ვერ ხერხდება."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s“"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"პასუხი გაეგზავნა %s-ს"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"სახელი მიუწვდომელია"</string>
+</resources>
diff --git a/car-messenger-common/res/values-kk/strings.xml b/car-messenger-common/res/values-kk/strings.xml
new file mode 100644
index 0000000..9132ef2
--- /dev/null
+++ b/car-messenger-common/res/values-kk/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d жаңа хабар</item>
+      <item quantity="one">Жаңа хабар</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Ойнату"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Оқылды деп белгілеу"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Қайталау"</string>
+    <string name="action_reply" msgid="564106590567600685">"Жауап беру"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Тоқтату"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Жабу"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Жауап жіберілмеді. Қайталап көріңіз."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Жауап жіберілмеді. Құрылғы жалғанбаған."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s дейді"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Хабар оқылмады."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Жауап %s атты пайдаланушыға жіберілді."</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Атауы жоқ"</string>
+</resources>
diff --git a/car-messenger-common/res/values-km/strings.xml b/car-messenger-common/res/values-km/strings.xml
new file mode 100644
index 0000000..b060c99
--- /dev/null
+++ b/car-messenger-common/res/values-km/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">សារ​ថ្មី %d</item>
+      <item quantity="one">សារ​ថ្មី</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"ចាក់"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"សម្គាល់​ថា​បានអាន​ហើយ"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"ធ្វើ​ឡើងវិញ"</string>
+    <string name="action_reply" msgid="564106590567600685">"ឆ្លើយតប"</string>
+    <string name="action_stop" msgid="6950369080845695405">"ឈប់"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"បិទ"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"មិនអាច​ផ្ញើ​ការឆ្លើយតប​បានទេ​។ សូមព្យាយាមម្ដងទៀត។"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"មិនអាច​ផ្ញើ​ការឆ្លើយតប​បានទេ​។ មិន​បាន​ភ្ជាប់​ឧបករណ៍​ទេ​។"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s និយាយ​ថា"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"មិនអាច​អានសារឱ្យឮៗ​បានទេ។"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"ការ​ឆ្លើយតប​ដែលបាន​ផ្ញើ​ទៅ %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"មិន​មាន​ឈ្មោះ​ទេ"</string>
+</resources>
diff --git a/car-messenger-common/res/values-kn/strings.xml b/car-messenger-common/res/values-kn/strings.xml
new file mode 100644
index 0000000..4b89c85
--- /dev/null
+++ b/car-messenger-common/res/values-kn/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d ಹೊಸ ಸಂದೇಶಗಳು</item>
+      <item quantity="other">%d ಹೊಸ ಸಂದೇಶಗಳು</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"ಪ್ಲೇ"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"ಓದಲಾಗಿದೆ ಎಂದು ಗುರುತಿಸಿ"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"ಪುನರಾವರ್ತನೆ"</string>
+    <string name="action_reply" msgid="564106590567600685">"ಪ್ರತ್ಯುತ್ತರಿಸಿ"</string>
+    <string name="action_stop" msgid="6950369080845695405">"ನಿಲ್ಲಿಸಿ"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"ಮುಚ್ಚಿರಿ"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"ಪ್ರತ್ಯುತ್ತರ ಕಳುಹಿಸಲು ವಿಫಲವಾಗಿದೆ. ಪುನಃ ಪ್ರಯತ್ನಿಸಿ."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"ಪ್ರತ್ಯುತ್ತರ ಕಳುಹಿಸಲು ವಿಫಲವಾಗಿದೆ. ಸಾಧನವು ಕನೆಕ್ಟ್ ಆಗಿಲ್ಲ."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s ಹೇಳುತ್ತದೆ"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"ಸಂದೇಶವನ್ನು ಓದಲು ಸಾಧ್ಯವಿಲ್ಲ."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s ಗೆ ಪ್ರತ್ಯುತ್ತರ ಕಳುಹಿಸಲಾಗಿದೆ"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"ಹೆಸರು ಲಭ್ಯವಿಲ್ಲ"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ko/strings.xml b/car-messenger-common/res/values-ko/strings.xml
new file mode 100644
index 0000000..1264304
--- /dev/null
+++ b/car-messenger-common/res/values-ko/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">새 메시지 %d개</item>
+      <item quantity="one">새 메시지</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"재생"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"읽은 상태로 표시"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"반복"</string>
+    <string name="action_reply" msgid="564106590567600685">"답장"</string>
+    <string name="action_stop" msgid="6950369080845695405">"중지"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"닫기"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"답장을 보낼 수 없습니다. 다시 시도해 보세요."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"답장을 보낼 수 없습니다. 기기가 연결되어 있지 않습니다."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s님의 말입니다"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"메시지를 소리 내어 읽을 수 없습니다."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"’%s’"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s님에게 답장을 전송했습니다."</string>
+    <string name="name_not_available" msgid="3800013092212550915">"이름을 사용할 수 없습니다."</string>
+</resources>
diff --git a/car-messenger-common/res/values-ky/strings.xml b/car-messenger-common/res/values-ky/strings.xml
new file mode 100644
index 0000000..4d33c71
--- /dev/null
+++ b/car-messenger-common/res/values-ky/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d жаңы билдирүү</item>
+      <item quantity="one">Жаңы билдирүү</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Ойнотуу"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Окулду деп белгилөө"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Кайталоо"</string>
+    <string name="action_reply" msgid="564106590567600685">"Жооп берүү"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Токтотуу"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Жабуу"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Жооп жөнөтүлгөн жок. Кайталап көрүңүз."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Жооп жөнөтүлгөн жок. Түзмөк туташкан жок."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s төмөнкүнү айтты:"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Билдирүү окулбай жатат."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Жооп төмөнкүгө жөнөтүлдү: %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Аты-жөнү жеткиликсиз"</string>
+</resources>
diff --git a/car-messenger-common/res/values-lo/strings.xml b/car-messenger-common/res/values-lo/strings.xml
new file mode 100644
index 0000000..db6627d
--- /dev/null
+++ b/car-messenger-common/res/values-lo/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d ຂໍ້ຄວາມໃໝ່</item>
+      <item quantity="one">ຂໍ້ຄວາມໃໝ່</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"ຫຼິ້ນ"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"ໝາຍວ່າອ່ານແລ້ວ"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"ເຮັດຊໍ້າຄືນ"</string>
+    <string name="action_reply" msgid="564106590567600685">"ຕອບກັບ"</string>
+    <string name="action_stop" msgid="6950369080845695405">"ຢຸດ"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"ປິດ"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"ບໍ່ສາມາດສົ່ງການຕອບກັບໄດ້. ກະລຸນາລອງໃໝ່."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"ບໍ່ສາມາດສົ່ງການຕອບກັບໄດ້. ອຸປະກອນບໍ່ໄດ້ເຊື່ອມຕໍ່."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s ເວົ້າວ່າ"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"ບໍ່ສາມາດອ່ານອອກສຽງຂໍ້ຄວາມໄດ້."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"ສົ່ງການຕອບກັບຫາ %s ແລ້ວ"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"ຊື່ບໍ່ສາມາດໃຊ້ໄດ້"</string>
+</resources>
diff --git a/car-messenger-common/res/values-lt/strings.xml b/car-messenger-common/res/values-lt/strings.xml
new file mode 100644
index 0000000..e9ba6cb
--- /dev/null
+++ b/car-messenger-common/res/values-lt/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d naujas pranešimas</item>
+      <item quantity="few">%d nauji pranešimai</item>
+      <item quantity="many">%d naujo pranešimo</item>
+      <item quantity="other">%d naujų pranešimų</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Leisti"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Pažymėti kaip skaitytą"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Kartoti"</string>
+    <string name="action_reply" msgid="564106590567600685">"Atsakyti"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Sustabdyti"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Uždaryti"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Nepavyko išsiųsti atsakymo. Bandykite dar kartą."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Nepavyko išsiųsti atsakymo. Įrenginys neprijungtas."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s sako"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Nepavyksta perskaityti pranešimo."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s“"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Atsakymas išsiųstas %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Vardas nepasiekiamas"</string>
+</resources>
diff --git a/car-messenger-common/res/values-lv/strings.xml b/car-messenger-common/res/values-lv/strings.xml
new file mode 100644
index 0000000..c6990ec
--- /dev/null
+++ b/car-messenger-common/res/values-lv/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="zero">%d jauni ziņojumi</item>
+      <item quantity="one">%d jauns ziņojums</item>
+      <item quantity="other">%d jauni ziņojumi</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Atskaņot"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Atzīmēt kā izlasītu"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Atkārtot"</string>
+    <string name="action_reply" msgid="564106590567600685">"Atbildēt"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Apturēt"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Aizvērt"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Nevar nosūtīt atbildi. Mēģiniet vēlreiz."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Nevar nosūtīt atbildi. Ierīce nav pievienota."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s saka"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Nevar nolasīt ziņojumu."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"“%s”"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Atbilde nosūtīta lietotājam %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Vārds nav pieejams."</string>
+</resources>
diff --git a/car-messenger-common/res/values-mk/strings.xml b/car-messenger-common/res/values-mk/strings.xml
new file mode 100644
index 0000000..12da0be
--- /dev/null
+++ b/car-messenger-common/res/values-mk/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d нова порака</item>
+      <item quantity="other">%d нови пораки</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Пушти"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Означи како прочитано"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Повтори"</string>
+    <string name="action_reply" msgid="564106590567600685">"Одговори"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Сопри"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Затвори"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Не може да се испрати одговор. Обидете се повторно."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Не може да се испрати одговор. Уредот не е поврзан."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s вели"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Не може да се прочита пораката на глас."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s“"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Одговорот е испратен до %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Името не е достапно"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ml/strings.xml b/car-messenger-common/res/values-ml/strings.xml
new file mode 100644
index 0000000..d154f97
--- /dev/null
+++ b/car-messenger-common/res/values-ml/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d പുതിയ സന്ദേശങ്ങൾ</item>
+      <item quantity="one">പുതിയ സന്ദേശം</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"പ്ലേ ചെയ്യുക"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"വായിച്ചതായി അടയാളപ്പെടുത്തുക"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"ആവർത്തിക്കുക"</string>
+    <string name="action_reply" msgid="564106590567600685">"മറുപടി നൽകുക"</string>
+    <string name="action_stop" msgid="6950369080845695405">"നിർത്തുക"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"അടയ്ക്കുക"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"മറുപടി അയയ്ക്കാനാവുന്നില്ല. വീണ്ടും ശ്രമിക്കുക."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"മറുപടി അയയ്ക്കാനാവുന്നില്ല. ഉപകരണം കണക്റ്റ് ചെയ്തിട്ടില്ല."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s പറയുന്നു"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"സന്ദേശം ഉറക്കെ വായിക്കാനാവില്ല."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s എന്നതിലേക്ക് മറുപടി അയച്ചു"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"പേര് ലഭ്യമല്ല"</string>
+</resources>
diff --git a/car-messenger-common/res/values-mn/strings.xml b/car-messenger-common/res/values-mn/strings.xml
new file mode 100644
index 0000000..d47453b
--- /dev/null
+++ b/car-messenger-common/res/values-mn/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d шинэ зурвас</item>
+      <item quantity="one">Шинэ зурвас</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Тоглуулах"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Уншсан гэж тэмдэглэх"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Давтах"</string>
+    <string name="action_reply" msgid="564106590567600685">"Хариу бичих"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Зогсоох"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Хаах"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Хариу илгээх боломжгүй байна. Дахин оролдоно уу."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Хариу илгээх боломжгүй байна. Төхөөрөмж холбогдоогүй байна."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s хэлж байна"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Зурвасыг унших боломжгүй байна."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s-д хариу илгээсэн"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Нэр ашиглалтад алга"</string>
+</resources>
diff --git a/car-messenger-common/res/values-mr/strings.xml b/car-messenger-common/res/values-mr/strings.xml
new file mode 100644
index 0000000..d81f6e9
--- /dev/null
+++ b/car-messenger-common/res/values-mr/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d नवीन मेसेज</item>
+      <item quantity="one">नवीन मेसेज</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"प्ले करा"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"वाचलेले म्हणून खूण करा"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"रिपीट करा"</string>
+    <string name="action_reply" msgid="564106590567600685">"उतर द्या"</string>
+    <string name="action_stop" msgid="6950369080845695405">"थांबा"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"बंद करा"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"उत्तर पाठवता आले नाही. कृपया पुन्हा प्रयत्न करा."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"उत्तर पाठवता आले नाही. डिव्हाइस कनेक्ट केलेले नाही."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s म्हणतात"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"मेसेज वाचू शकत नाही."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"उत्तर %s ला पाठवले"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"नाव उपलब्ध नाही"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ms/strings.xml b/car-messenger-common/res/values-ms/strings.xml
new file mode 100644
index 0000000..ff4041c
--- /dev/null
+++ b/car-messenger-common/res/values-ms/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d mesej baharu</item>
+      <item quantity="one">Mesej baharu</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Main"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Tandai Sebagai Dibaca"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Ulang"</string>
+    <string name="action_reply" msgid="564106590567600685">"Balas"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Berhenti"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Tutup"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Tidak dapat menghantar balasan. Sila cuba lagi."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Tidak dapat menghantar balasan. Peranti tidak disambungkan."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s mengatakan"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Tidak dapat membaca mesej dengan kuat."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Balasan dihantar kepada %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nama tidak tersedia"</string>
+</resources>
diff --git a/car-messenger-common/res/values-my/strings.xml b/car-messenger-common/res/values-my/strings.xml
new file mode 100644
index 0000000..9604313
--- /dev/null
+++ b/car-messenger-common/res/values-my/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">မက်ဆေ့ဂျ်အသစ် %d စောင်</item>
+      <item quantity="one">မက်ဆေ့ဂျ်အသစ်</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"ဖွင့်ရန်"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"ဖတ်ပြီးဟု မှတ်သားရန်"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"ထပ်လုပ်ရန်"</string>
+    <string name="action_reply" msgid="564106590567600685">"စာပြန်ရန်"</string>
+    <string name="action_stop" msgid="6950369080845695405">"ရပ်ရန်"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"ပိတ်ရန်"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"ပြန်စာကို ပို့၍မရပါ။ ထပ်စမ်းကြည့်ပါ။"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"ပြန်စာကို ပို့၍မရပါ။ စက်ကို ကွန်ရက်ချိတ်မထားပါ။"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s ကပြောသည်မှာ"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"မက်ဆေ့ဂျ်ကို အသံထွက်ဖတ်၍မရပါ။"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"ပြန်စာကို %s သို့ ပို့လိုက်ပါပြီ"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"အမည် မရနိုင်ပါ"</string>
+</resources>
diff --git a/car-messenger-common/res/values-nb/strings.xml b/car-messenger-common/res/values-nb/strings.xml
new file mode 100644
index 0000000..70be831
--- /dev/null
+++ b/car-messenger-common/res/values-nb/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d nye meldinger</item>
+      <item quantity="one">Ny melding</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Spill av"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Merk som lest"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Gjenta"</string>
+    <string name="action_reply" msgid="564106590567600685">"Svar"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stopp"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Lukk"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Kan ikke sende svaret. Prøv på nytt."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Kan ikke sende svaret. Enheten er ikke tilkoblet."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s sier"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Kan ikke lese opp meldingen."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"«%s»"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Svaret er sendt til %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Navnet er ikke tilgjengelig"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ne/strings.xml b/car-messenger-common/res/values-ne/strings.xml
new file mode 100644
index 0000000..26e1e7c
--- /dev/null
+++ b/car-messenger-common/res/values-ne/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d नयाँ सन्देशहरू</item>
+      <item quantity="one">नयाँ सन्देश</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"प्ले गर्नुहोस्"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"पढिसकिएको भनी चिन्ह लगाउनुहोस्"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"दोहोर्‍याउनुहोस्"</string>
+    <string name="action_reply" msgid="564106590567600685">"जवाफ पठाउनुहोस्"</string>
+    <string name="action_stop" msgid="6950369080845695405">"रोक्नुहोस्"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"बन्द गर्नुहोस्"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"जवाफ पठाउन सकिएन। कृपया फेरि प्रयास गर्नुहोस्।"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"जवाफ पठाउन सकिएन। यन्त्र जोडिएको छैन।"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s निम्न कुरा भन्नुहुन्छ:"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"सन्देशहरू पढ्न सकिँदैन।"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s मा जवाफ पठाइयो"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"नाम उपलब्ध छैन"</string>
+</resources>
diff --git a/car-messenger-common/res/values-nl/strings.xml b/car-messenger-common/res/values-nl/strings.xml
new file mode 100644
index 0000000..42aff7a
--- /dev/null
+++ b/car-messenger-common/res/values-nl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d nieuwe berichten</item>
+      <item quantity="one">Nieuw bericht</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Afspelen"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Markeren als gelezen"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Herhalen"</string>
+    <string name="action_reply" msgid="564106590567600685">"Beantwoorden"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stoppen"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Sluiten"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Antwoord kan niet worden verstuurd. Probeer het opnieuw."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Antwoord kan niet worden verstuurd. Apparaat is niet verbonden."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s zegt"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Kan bericht niet voorlezen."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\'%s\'"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Antwoord naar %s gestuurd"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Naam niet beschikbaar"</string>
+</resources>
diff --git a/car-messenger-common/res/values-or/strings.xml b/car-messenger-common/res/values-or/strings.xml
new file mode 100644
index 0000000..168f3ae
--- /dev/null
+++ b/car-messenger-common/res/values-or/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%dଟି ନୂଆ ମେସେଜ୍</item>
+      <item quantity="one">ନୂଆ ମେସେଜ୍</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"ଚଲାନ୍ତୁ"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"ପଠିତ ଭାବେ ଚିହ୍ନଟ କରନ୍ତୁ"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"ପୁନରାବୃତ୍ତି କରନ୍ତୁ"</string>
+    <string name="action_reply" msgid="564106590567600685">"ପ୍ରତ୍ୟୁତ୍ତର କରନ୍ତୁ"</string>
+    <string name="action_stop" msgid="6950369080845695405">"ବନ୍ଦ କରନ୍ତୁ"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"ବନ୍ଦ କରନ୍ତୁ"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"ପ୍ରତ୍ୟୁତ୍ତର ପଠାଇବାକୁ ଅସମର୍ଥ। ଦୟାକରି ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ।"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"ପ୍ରତ୍ୟୁତ୍ତର ପଠାଇବାକୁ ଅସମର୍ଥ। ଡିଭାଇସ୍ ସଂଯୋଗ ହୋଇନାହିଁ।"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s କୁହେ"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"ମେସେଜ୍ ପଢ଼ାଯାଇପାରିବ ନାହିଁ।"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%sକୁ ପ୍ରତ୍ୟୁତ୍ତର ପଠାଯାଇଛି"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"ନାମ ଉପଲବ୍ଧ ନାହିଁ"</string>
+</resources>
diff --git a/car-messenger-common/res/values-pa/strings.xml b/car-messenger-common/res/values-pa/strings.xml
new file mode 100644
index 0000000..96799b9
--- /dev/null
+++ b/car-messenger-common/res/values-pa/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d ਨਵਾਂ ਸੁਨੇਹਾ</item>
+      <item quantity="other">%d ਨਵੇਂ ਸੁਨੇਹੇ</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"ਚਲਾਓ"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"ਪੜ੍ਹੇ ਵਜੋਂ ਨਿਸ਼ਾਨਦੇਹੀ ਕਰੋ"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"ਦੁਹਰਾਓ"</string>
+    <string name="action_reply" msgid="564106590567600685">"ਜਵਾਬ ਦਿਓ"</string>
+    <string name="action_stop" msgid="6950369080845695405">"ਬੰਦ ਕਰੋ"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"ਬੰਦ ਕਰੋ"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"ਜਵਾਬ ਭੇਜਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ। ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"ਜਵਾਬ ਭੇਜਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ। ਡੀਵਾਈਸ ਕਨੈਕਟ ਨਹੀਂ ਹੈ।"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s ਕਹਿੰਦਾ ਹੈ"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"ਸੁਨੇਹਾ ਪੜ੍ਹਿਆ ਨਹੀਂ ਜਾ ਸਕਦਾ।"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s ਨੂੰ ਜਵਾਬ ਭੇਜਿਆ ਗਿਆ"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"ਨਾਮ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>
+</resources>
diff --git a/car-messenger-common/res/values-pl/strings.xml b/car-messenger-common/res/values-pl/strings.xml
new file mode 100644
index 0000000..b8df4ec
--- /dev/null
+++ b/car-messenger-common/res/values-pl/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="few">%d nowe wiadomości</item>
+      <item quantity="many">%d nowych wiadomości</item>
+      <item quantity="other">%d nowej wiadomości</item>
+      <item quantity="one">Nowa wiadomość</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Odtwórz"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Oznacz jako przeczytane"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Powtórz"</string>
+    <string name="action_reply" msgid="564106590567600685">"Odpowiedz"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Zatrzymaj"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Zamknij"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Nie udało się wysłać odpowiedzi. Spróbuj ponownie."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Nie udało się wysłać odpowiedzi. Urządzenie nie ma połączenia."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s mówi"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Nie mogę odczytać wiadomości."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s”"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Wysłano odpowiedź do: %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nazwa niedostępna"</string>
+</resources>
diff --git a/car-messenger-common/res/values-pt-rPT/strings.xml b/car-messenger-common/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..b0da748
--- /dev/null
+++ b/car-messenger-common/res/values-pt-rPT/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d novas mensagens</item>
+      <item quantity="one">Nova mensagem</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Reproduzir"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Marcar como lida"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repetir"</string>
+    <string name="action_reply" msgid="564106590567600685">"Responder"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Parar"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Fechar"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Não foi possível enviar a resposta. Tente novamente."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Não foi possível enviar a resposta. O dispositivo não está ligado."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s diz"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Não é possível ler a mensagem em voz alta."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Resposta enviada para %s."</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nome não disponível."</string>
+</resources>
diff --git a/car-messenger-common/res/values-pt/strings.xml b/car-messenger-common/res/values-pt/strings.xml
new file mode 100644
index 0000000..90d2171
--- /dev/null
+++ b/car-messenger-common/res/values-pt/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d nova mensagem</item>
+      <item quantity="other">%d novas mensagens</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Ouvir"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Marcar como lida"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repetir"</string>
+    <string name="action_reply" msgid="564106590567600685">"Responder"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Parar"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Fechar"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Não foi possível enviar a resposta. Tente novamente."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Não foi possível enviar a resposta. Dispositivo não conectado."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s disse"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Não é possível ler a mensagem em voz alta."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Resposta enviada para %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Nome indisponível"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ro/strings.xml b/car-messenger-common/res/values-ro/strings.xml
new file mode 100644
index 0000000..87fe506
--- /dev/null
+++ b/car-messenger-common/res/values-ro/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="few">%d mesaje noi</item>
+      <item quantity="other">%d de mesaje noi</item>
+      <item quantity="one">Mesaj nou</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Redați"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Marcați mesajul drept citit"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Repetați"</string>
+    <string name="action_reply" msgid="564106590567600685">"Răspundeți"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Opriți"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Închideți"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Nu se poate trimite răspunsul. Încercați din nou."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Nu se poate trimite răspunsul. Dispozitivul nu este conectat."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s spune"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Nu se poate citi mesajul."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s”"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Răspuns trimis la %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Numele nu este disponibil"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ru/strings.xml b/car-messenger-common/res/values-ru/strings.xml
new file mode 100644
index 0000000..bfa047d
--- /dev/null
+++ b/car-messenger-common/res/values-ru/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d новое сообщение</item>
+      <item quantity="few">%d новых сообщения</item>
+      <item quantity="many">%d новых сообщений</item>
+      <item quantity="other">%d новых сообщения</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Воспроизвести"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Прочитано"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Повторить"</string>
+    <string name="action_reply" msgid="564106590567600685">"Ответить"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Остановить"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Закрыть"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Не удалось отправить ответ. Повторите попытку."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Не удалось отправить ответ. Устройство не подключено."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s говорит"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Не удалось прочитать сообщение."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Ответ отправлен пользователю %s."</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Имя недоступно"</string>
+</resources>
diff --git a/car-messenger-common/res/values-si/strings.xml b/car-messenger-common/res/values-si/strings.xml
new file mode 100644
index 0000000..fdab619
--- /dev/null
+++ b/car-messenger-common/res/values-si/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">නව පණිවුඩ %d ක්</item>
+      <item quantity="other">නව පණිවුඩ %d ක්</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"ධාවනය"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"කියවූ ලෙස ලකුණු කරන්න"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"නැවත කරන්න"</string>
+    <string name="action_reply" msgid="564106590567600685">"පිළිතුරු දෙන්න"</string>
+    <string name="action_stop" msgid="6950369080845695405">"නවත්වන්න"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"වසන්න"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"පිළිතුර යැවිය නොහැක. නැවත උත්සාහ කරන්න."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"පිළිතුර යැවිය නොහැක. උපාංගය සම්බන්ධ කර නැත."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s කියන්නේ"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"පණිවිඩය කියවිය නොහැක."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"පිළිතුර %s වෙත යැවුවා"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"නම නොලැබේ"</string>
+</resources>
diff --git a/car-messenger-common/res/values-sk/strings.xml b/car-messenger-common/res/values-sk/strings.xml
new file mode 100644
index 0000000..7053f2c
--- /dev/null
+++ b/car-messenger-common/res/values-sk/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="few">%d nové správy</item>
+      <item quantity="many">%d new messages</item>
+      <item quantity="other">%d nových správ</item>
+      <item quantity="one">Nová správa</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Prehrať"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Označiť ako prečítané"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Opakovať"</string>
+    <string name="action_reply" msgid="564106590567600685">"Odpovedať"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Zastaviť"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Zavrieť"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Odpoveď sa nedá odoslať. Skúste to znova."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Odpoveď sa nedá odoslať. Zariadenie nie je pripojené."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s hovorí"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Správu sa nepodarilo prečítať."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"%s"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Odpoveď bola odoslaná do systému %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Meno nie je k dispozícii"</string>
+</resources>
diff --git a/car-messenger-common/res/values-sl/strings.xml b/car-messenger-common/res/values-sl/strings.xml
new file mode 100644
index 0000000..8dab1c4
--- /dev/null
+++ b/car-messenger-common/res/values-sl/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d novo sporočilo</item>
+      <item quantity="two">%d novi sporočili</item>
+      <item quantity="few">%d nova sporočila</item>
+      <item quantity="other">%d novih sporočil</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Predvajaj"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Označi kot prebrano"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Ponovi"</string>
+    <string name="action_reply" msgid="564106590567600685">"Odgovori"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Ustavi"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Zapri"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Odgovora ni mogoče poslati. Poskusite znova."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Odgovora ni mogoče poslati. Naprava ni povezana."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s pravi"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Sporočila ni mogoče prebrati na glas."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"»%s«"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Odgovor poslan osebi %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Ime ni na voljo"</string>
+</resources>
diff --git a/car-messenger-common/res/values-sq/strings.xml b/car-messenger-common/res/values-sq/strings.xml
new file mode 100644
index 0000000..eb33473
--- /dev/null
+++ b/car-messenger-common/res/values-sq/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d mesazhe të reja</item>
+      <item quantity="one">Mesazh i ri</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Luaj"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Shëno si të lexuar"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Përsërit"</string>
+    <string name="action_reply" msgid="564106590567600685">"Përgjigju"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Ndalo"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Mbyll"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Nuk mund të dërgohet. Provo përsëri."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Nuk mund të dërgohet. Pajisja nuk është e lidhur."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s thotë"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Mesazhi nuk mund të lexohet me zë."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Përgjigjja u dërgua te %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Emri nuk ofrohet"</string>
+</resources>
diff --git a/car-messenger-common/res/values-sr/strings.xml b/car-messenger-common/res/values-sr/strings.xml
new file mode 100644
index 0000000..19601c9
--- /dev/null
+++ b/car-messenger-common/res/values-sr/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d нова порука</item>
+      <item quantity="few">%d нове поруке</item>
+      <item quantity="other">%d нових порука</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Пусти"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Означи као прочитано"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Понови"</string>
+    <string name="action_reply" msgid="564106590567600685">"Одговори"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Заустави"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Затвори"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Слање одговора није успело. Пробајте поново."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Слање одговора није успело. Уређај није повезан."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s каже"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Читање поруке наглас није успело."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"„%s“"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Одговор је послат контакту %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Име није доступно"</string>
+</resources>
diff --git a/car-messenger-common/res/values-sv/strings.xml b/car-messenger-common/res/values-sv/strings.xml
new file mode 100644
index 0000000..ef6dd39
--- /dev/null
+++ b/car-messenger-common/res/values-sv/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d nya meddelanden</item>
+      <item quantity="one">Nytt meddelande</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Spela upp"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Markera som läst"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Upprepa"</string>
+    <string name="action_reply" msgid="564106590567600685">"Svara"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Stopp"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Stäng"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Det gick inte att skicka svaret. Försök igen."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Det gick inte att skicka svaret. Enheten är inte ansluten."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s säger"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Det går inte att läsa upp meddelandet."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"%s"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Svaret har skickats till %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Namnet är inte tillgängligt"</string>
+</resources>
diff --git a/car-messenger-common/res/values-sw/strings.xml b/car-messenger-common/res/values-sw/strings.xml
new file mode 100644
index 0000000..4edf7cd
--- /dev/null
+++ b/car-messenger-common/res/values-sw/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">Ujumbe %d mpya</item>
+      <item quantity="one">Ujumbe mpya</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Cheza"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Tia Alama Kuwa Umesomwa"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Rudia"</string>
+    <string name="action_reply" msgid="564106590567600685">"Jibu"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Komesha"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Funga"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Imeshindwa kutuma jibu. Tafadhali jaribu tena."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Imeshindwa kutuma jibu. Kifaa hakijaunganishwa."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s anasema"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Imeshindwa kusoma ujumbe."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Jibu limetumwa kwa %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Jina halipatikani"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ta/strings.xml b/car-messenger-common/res/values-ta/strings.xml
new file mode 100644
index 0000000..2bd1e27
--- /dev/null
+++ b/car-messenger-common/res/values-ta/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d புதிய மெசேஜ்கள்</item>
+      <item quantity="one">புதிய மெசேஜ்</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"பிளே செய்"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"படித்ததாகக் குறி"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"மீண்டும்"</string>
+    <string name="action_reply" msgid="564106590567600685">"பதிலளி"</string>
+    <string name="action_stop" msgid="6950369080845695405">"நிறுத்து"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"மூடுக"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"பதிலை அனுப்ப முடியவில்லை. மீண்டும் முயலவும்."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"பதிலை அனுப்ப முடியவில்லை. சாதனம் இணைக்கப்படவில்லை."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s மெசேஜ்"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"மெசேஜைப் படிக்க முடியவில்லை."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%sக்கு பதில் அனுப்பப்பட்டது"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"பெயர் கிடைக்கவில்லை"</string>
+</resources>
diff --git a/car-messenger-common/res/values-te/strings.xml b/car-messenger-common/res/values-te/strings.xml
new file mode 100644
index 0000000..9dfa1ab
--- /dev/null
+++ b/car-messenger-common/res/values-te/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d కొత్త సందేశాలు</item>
+      <item quantity="one">కొత్త సందేశం</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"ప్లే చేయి"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"చదివినట్లు గుర్తు పెట్టు"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"పునరావృతం చేయి"</string>
+    <string name="action_reply" msgid="564106590567600685">"ప్రత్యుత్తరమివ్వు"</string>
+    <string name="action_stop" msgid="6950369080845695405">"ఆపివేయి"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"మూసివేయి"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"ప్రత్యుత్తరం పంపడం సాధ్యం కాలేదు. దయచేసి మళ్లీ ప్రయత్నించండి."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"ప్రత్యుత్తరం పంపడం సాధ్యం కాలేదు. పరికరం కనెక్ట్ కాలేదు."</string>
+    <!-- String.format failed for translation -->
+    <!-- no translation found for tts_sender_says (5352698006545359668) -->
+    <skip />
+    <string name="tts_failed_toast" msgid="1483313550894086353">"సందేశాన్ని చదవడం సాధ్యం కాలేదు."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s కు ప్రత్యుత్తరం పంపబడింది"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"పేరు అందుబాటులో లేదు"</string>
+</resources>
diff --git a/car-messenger-common/res/values-th/strings.xml b/car-messenger-common/res/values-th/strings.xml
new file mode 100644
index 0000000..b6cfc1e
--- /dev/null
+++ b/car-messenger-common/res/values-th/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">ข้อความใหม่ %d ข้อความ</item>
+      <item quantity="one">ข้อความใหม่</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"เล่น"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"ทำเครื่องหมายว่าอ่านแล้ว"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"เล่นซ้ำ"</string>
+    <string name="action_reply" msgid="564106590567600685">"ตอบ"</string>
+    <string name="action_stop" msgid="6950369080845695405">"หยุด"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"ปิด"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"ส่งการตอบกลับไม่ได้ โปรดลองอีกครั้ง"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"ส่งการตอบกลับไม่ได้ อุปกรณ์ไม่ได้เชื่อมต่ออยู่"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s บอกว่า"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"อ่านออกเสียงข้อความไม่ได้"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"ส่งการตอบกลับถึง %s แล้ว"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"ไม่มีชื่อ"</string>
+</resources>
diff --git a/car-messenger-common/res/values-tl/strings.xml b/car-messenger-common/res/values-tl/strings.xml
new file mode 100644
index 0000000..0a09495
--- /dev/null
+++ b/car-messenger-common/res/values-tl/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d bagong mensahe</item>
+      <item quantity="other">%d na bagong mensahe</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"I-play"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Markahan Bilang Nabasa Na"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Ulitin"</string>
+    <string name="action_reply" msgid="564106590567600685">"Sumagot"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Ihinto"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Isara"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Hindi maipadala ang sagot. Pakisubukan ulit."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Hindi maipadala ang sagot. Hindi nakakonekta ang device."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"Sabi ni %s,"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Hindi mabasa ang mensahe."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Naipadala ang sagot kay %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Hindi available ang pangalan"</string>
+</resources>
diff --git a/car-messenger-common/res/values-tr/strings.xml b/car-messenger-common/res/values-tr/strings.xml
new file mode 100644
index 0000000..8c8126c
--- /dev/null
+++ b/car-messenger-common/res/values-tr/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d yeni mesaj</item>
+      <item quantity="one">Yeni mesaj</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Oynat"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Okundu Olarak İşaretle"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Tekrar"</string>
+    <string name="action_reply" msgid="564106590567600685">"Yanıtla"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Durdur"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Kapat"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Yanıt gönderilemedi. Lütfen tekrar deneyin."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Yanıt gönderilemedi. Cihaz bağlı değil."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s diyor ki"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Mesaj sesli okunamıyor."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"%s adlı kişiye yanıt gönderilemedi"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Ad gösterilemiyor"</string>
+</resources>
diff --git a/car-messenger-common/res/values-uk/strings.xml b/car-messenger-common/res/values-uk/strings.xml
new file mode 100644
index 0000000..0b46853
--- /dev/null
+++ b/car-messenger-common/res/values-uk/strings.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d нове повідомлення</item>
+      <item quantity="few">%d нові повідомлення</item>
+      <item quantity="many">%d нових повідомлень</item>
+      <item quantity="other">%d нового повідомлення</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Відтворити"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Позначити як прочитане"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Повторити"</string>
+    <string name="action_reply" msgid="564106590567600685">"Відповісти"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Зупинити"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Закрити"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Неможливо надіслати відповідь. Повторіть спробу."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Неможливо надіслати відповідь. Пристрій не підключено."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"Повідомлення від користувача %s"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Не вдалося озвучити повідомлення."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Відповідь, надіслана користувачу %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Назва недоступна"</string>
+</resources>
diff --git a/car-messenger-common/res/values-ur/strings.xml b/car-messenger-common/res/values-ur/strings.xml
new file mode 100644
index 0000000..542b43c
--- /dev/null
+++ b/car-messenger-common/res/values-ur/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">‎%d نئے پیغامات</item>
+      <item quantity="one">نیا پیغام</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"چلائیں"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"پڑھا ہوا کے بطور نشان زد کریں"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"مکرر"</string>
+    <string name="action_reply" msgid="564106590567600685">"جواب دیں"</string>
+    <string name="action_stop" msgid="6950369080845695405">"روکیں"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"بند کریں"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"جواب بھیجنے سے قاصر۔ براہ کرم دوبارہ کوشش کریں۔"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"جواب بھیجنے سے قاصر۔ آلہ منسلک نہیں ہے۔"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s کا کہنا ہے"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"پیغام نہیں پڑھا جا سکتا۔"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"جواب %s پر بھیجا گیا"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"نام دستیاب نہیں ہے"</string>
+</resources>
diff --git a/car-messenger-common/res/values-uz/strings.xml b/car-messenger-common/res/values-uz/strings.xml
new file mode 100644
index 0000000..d886171
--- /dev/null
+++ b/car-messenger-common/res/values-uz/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d ta yangi xabar</item>
+      <item quantity="one">Yangi xabar</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Ijro"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Ochilgan deb belgilash"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Takrorlash"</string>
+    <string name="action_reply" msgid="564106590567600685">"Javob berish"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Toʻxtatish"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Yopish"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Javob yuborilmadi. Qayta urining."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Javob yuborilmadi. Qurilma ulanmagan."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s dedi"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Xabar oʻqilmadi."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"“%s”"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Javob bunga yuborildi: %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Bu nom mavjud emas"</string>
+</resources>
diff --git a/car-messenger-common/res/values-vi/strings.xml b/car-messenger-common/res/values-vi/strings.xml
new file mode 100644
index 0000000..bc23c3e
--- /dev/null
+++ b/car-messenger-common/res/values-vi/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d tin nhắn mới</item>
+      <item quantity="one">Tin nhắn mới</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Phát"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Đánh dấu là đã đọc"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Lặp lại"</string>
+    <string name="action_reply" msgid="564106590567600685">"Trả lời"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Dừng"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Đóng"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Không gửi được nội dung trả lời. Vui lòng thử lại."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Không gửi được nội dung trả lời. Thiết bị chưa được kết nối."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s nhắn"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Không thể đọc to thông báo."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Đã gửi nội dung trả lời tới %s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Không có tên này"</string>
+</resources>
diff --git a/car-messenger-common/res/values-zh-rCN/strings.xml b/car-messenger-common/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..97088d9
--- /dev/null
+++ b/car-messenger-common/res/values-zh-rCN/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d 条新消息</item>
+      <item quantity="one">1 条新消息</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"播放"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"标记为已读"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"重复"</string>
+    <string name="action_reply" msgid="564106590567600685">"回复"</string>
+    <string name="action_stop" msgid="6950369080845695405">"停止"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"关闭"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"无法发送回复。请重试。"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"无法发送回复。设备未连接。"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s说"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"无法读出消息。"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"“%s”"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"已将回复发送给%s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"名称不可用"</string>
+</resources>
diff --git a/car-messenger-common/res/values-zh-rHK/strings.xml b/car-messenger-common/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..bdc6a72
--- /dev/null
+++ b/car-messenger-common/res/values-zh-rHK/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d 個新訊息</item>
+      <item quantity="one">新訊息</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"播放"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"標示為已讀"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"重複"</string>
+    <string name="action_reply" msgid="564106590567600685">"回覆"</string>
+    <string name="action_stop" msgid="6950369080845695405">"停止"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"關閉"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"無法傳送回覆,請再試一次。"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"無法傳送回覆,裝置未連接。"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"%s話"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"無法讀出訊息。"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"「%s」"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"已向%s傳送回覆"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"找不到名稱"</string>
+</resources>
diff --git a/car-messenger-common/res/values-zh-rTW/strings.xml b/car-messenger-common/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..a94a58b
--- /dev/null
+++ b/car-messenger-common/res/values-zh-rTW/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="other">%d 則新訊息</item>
+      <item quantity="one">新訊息</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"播放"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"標示為已讀取"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"重複播放"</string>
+    <string name="action_reply" msgid="564106590567600685">"回覆"</string>
+    <string name="action_stop" msgid="6950369080845695405">"停止"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"關閉"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"無法傳送回覆,請再試一次。"</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"尚未與裝置連線,因此無法傳送回覆。"</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"「%s」說:"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"無法朗讀訊息。"</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"「%s」"</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"已將回覆傳送給「%s」"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"無法使用這個名稱"</string>
+</resources>
diff --git a/car-messenger-common/res/values-zu/strings.xml b/car-messenger-common/res/values-zu/strings.xml
new file mode 100644
index 0000000..1292512
--- /dev/null
+++ b/car-messenger-common/res/values-zu/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ 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.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <plurals name="notification_new_message" formatted="false" msgid="1631343923556571689">
+      <item quantity="one">%d imilayezo emisha</item>
+      <item quantity="other">%d imilayezo emisha</item>
+    </plurals>
+    <string name="action_play" msgid="1884580550634079470">"Dlala"</string>
+    <string name="action_mark_as_read" msgid="5185216939940407938">"Maka njengokufundiwe"</string>
+    <string name="action_repeat" msgid="8184323082093728957">"Phinda"</string>
+    <string name="action_reply" msgid="564106590567600685">"Phendula"</string>
+    <string name="action_stop" msgid="6950369080845695405">"Misa"</string>
+    <string name="action_close_messages" msgid="7949295965012770696">"Vala"</string>
+    <string name="auto_reply_failed_message" msgid="6445984971657465627">"Ayikwazi ukuthumela impendulo. Sicela uzame futhi."</string>
+    <string name="auto_reply_device_disconnected" msgid="5861772755278229950">"Ayikwazi ukuthumela impendulo. Idivayisi ayixhunyiwe."</string>
+    <string name="tts_sender_says" msgid="5352698006545359668">"U-%s uthi"</string>
+    <string name="tts_failed_toast" msgid="1483313550894086353">"Ayikwazi ukufundela phezulu umlayezo."</string>
+    <string name="reply_message_display_template" msgid="6348622926232346974">"\"%s\""</string>
+    <string name="message_sent_notice" msgid="7172592196465284673">"Impendulo ithunyelwe ku-%s"</string>
+    <string name="name_not_available" msgid="3800013092212550915">"Igama alitholakali"</string>
+</resources>
diff --git a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml b/car-messenger-common/res/values/dimens.xml
similarity index 62%
copy from car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
copy to car-messenger-common/res/values/dimens.xml
index 19a1111..ea87725 100644
--- a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
+++ b/car-messenger-common/res/values/dimens.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
+<?xml version='1.0' encoding='UTF-8'?>
 <!--
   ~ Copyright (C) 2019 The Android Open Source Project
   ~
@@ -14,12 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <com.android.car.ui.toolbar.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-</FrameLayout>
+<resources>
+    <dimen name="notification_contact_photo_size">300dp</dimen>
+    <dimen name="contact_avatar_corner_radius_percent" format="float">0.5</dimen>
+</resources>
diff --git a/car-messenger-common/res/values/strings.xml b/car-messenger-common/res/values/strings.xml
new file mode 100644
index 0000000..ff604e2
--- /dev/null
+++ b/car-messenger-common/res/values/strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<resources>
+    <plurals name="notification_new_message">
+        <item quantity="one">New message</item>
+        <item quantity="other">%d new messages</item>
+    </plurals>
+
+    <string name="action_play">Play</string>
+    <string name="action_mark_as_read">Mark As Read</string>
+    <string name="action_repeat">Repeat</string>
+    <string name="action_reply">Reply</string>
+    <string name="action_stop">Stop</string>
+    <string name="action_close_messages">Close</string>
+    <string name="auto_reply_failed_message">Unable to send reply. Please try again.</string>
+    <string name="auto_reply_device_disconnected">Unable to send reply. Device is not connected.
+    </string>
+
+    <string name="tts_sender_says">%s says</string>
+
+    <string name="tts_failed_toast">Can\'t read out message.</string>
+    <string name="reply_message_display_template">\"%s\"</string>
+    <string name="message_sent_notice">Reply sent to %s</string>
+
+    <!-- Default Sender name that appears in message notification if sender name is not available. [CHAR_LIMIT=NONE] -->
+    <string name="name_not_available">Name not available</string>
+
+    <!-- Formats a group conversation's title for a message notification. The format is: <Sender of last message> mdot <Name of the conversation>.-->
+    <string name="group_conversation_title_separator" translatable="false">%1$s&#160;&#8226;&#160;%2$s</string>
+
+</resources>
diff --git a/car-messenger-common/src/com/android/car/messenger/common/BaseNotificationDelegate.java b/car-messenger-common/src/com/android/car/messenger/common/BaseNotificationDelegate.java
new file mode 100644
index 0000000..3045cdc
--- /dev/null
+++ b/car-messenger-common/src/com/android/car/messenger/common/BaseNotificationDelegate.java
@@ -0,0 +1,334 @@
+/*
+ * 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.messenger.common;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationCompat.Action;
+import androidx.core.app.Person;
+
+import com.android.car.apps.common.LetterTileDrawable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+/**
+ * Base Interface for Message Notification Delegates.
+ * Any Delegate who chooses to extend from this class is responsible for:
+ * <p> device connection logic </p>
+ * <p> sending and receiving messages from the connected devices </p>
+ * <p> creation of {@link ConversationNotificationInfo} and {@link Message} objects </p>
+ * <p> creation of {@link ConversationKey}, {@link MessageKey}, {@link SenderKey} </p>
+ * <p> loading of largeIcons for each Sender per device </p>
+ * <p> Mark-as-Read and Reply functionality  </p>
+ **/
+public class BaseNotificationDelegate {
+
+    /** Used to reply to message. */
+    public static final String ACTION_REPLY = "com.android.car.messenger.common.ACTION_REPLY";
+
+    /** Used to clear notification state when user dismisses notification. */
+    public static final String ACTION_DISMISS_NOTIFICATION =
+            "com.android.car.messenger.common.ACTION_DISMISS_NOTIFICATION";
+
+    /** Used to mark a notification as read **/
+    public static final String ACTION_MARK_AS_READ =
+            "com.android.car.messenger.common.ACTION_MARK_AS_READ";
+
+    /* EXTRAS */
+    /** Key under which the {@link ConversationKey} is provided. */
+    public static final String EXTRA_CONVERSATION_KEY =
+            "com.android.car.messenger.common.EXTRA_CONVERSATION_KEY";
+
+    /**
+     * The resultKey of the {@link RemoteInput} which is sent in the reply callback {@link
+     * Notification.Action}.
+     */
+    public static final String EXTRA_REMOTE_INPUT_KEY =
+            "com.android.car.messenger.common.REMOTE_INPUT_KEY";
+
+    protected final Context mContext;
+    protected final String mClassName;
+    protected final NotificationManager mNotificationManager;
+    protected final boolean mUseLetterTile;
+
+    /**
+     * Maps a conversation's Notification Metadata to the conversation's unique key.
+     * The extending class should always keep this map updated with the latest new/updated
+     * notification information before calling {@link BaseNotificationDelegate#postNotification(
+     * ConversationKey, ConversationNotificationInfo, String)}.
+     **/
+    protected final Map<ConversationKey, ConversationNotificationInfo> mNotificationInfos =
+            new HashMap<>();
+
+    /**
+     * Maps a conversation's Notification Builder to the conversation's unique key. When the
+     * conversation gets updated, this builder should be retrieved, updated, and reposted.
+     **/
+    private final Map<ConversationKey, NotificationCompat.Builder> mNotificationBuilders =
+            new HashMap<>();
+
+    /**
+     * Maps a message's metadata with the message's unique key.
+     * The extending class should always keep this map updated with the latest message information
+     * before calling {@link BaseNotificationDelegate#postNotification(
+     * ConversationKey, ConversationNotificationInfo, String)}.
+     **/
+    protected final Map<MessageKey, Message> mMessages = new HashMap<>();
+
+    /**
+     * Maps a Bitmap of a sender's Large Icon to the sender's unique key.
+     * The extending class should always keep this map updated with the loaded Sender large icons
+     * before calling {@link BaseNotificationDelegate#postNotification(
+     * ConversationKey, ConversationNotificationInfo, String)}. If the large icon is not found for
+     * the {@link SenderKey} when constructing the notification, a {@link LetterTileDrawable} will
+     * be created for the sender, unless {@link BaseNotificationDelegate#mUseLetterTile} is set to
+     * false.
+     **/
+    protected final Map<SenderKey, Bitmap> mSenderLargeIcons = new HashMap<>();
+
+    private final int mBitmapSize;
+    private final float mCornerRadiusPercent;
+
+    /**
+     * Constructor for the BaseNotificationDelegate class.
+     * @param context of the calling application.
+     * @param className of the calling application.
+     * @param useLetterTile whether a letterTile icon should be used if no avatar icon is given.
+     **/
+    public BaseNotificationDelegate(Context context, String className, boolean useLetterTile) {
+        mContext = context;
+        mClassName = className;
+        mUseLetterTile = useLetterTile;
+        mNotificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        mBitmapSize =
+                mContext.getResources()
+                        .getDimensionPixelSize(R.dimen.notification_contact_photo_size);
+        mCornerRadiusPercent = mContext.getResources()
+                .getFloat(R.dimen.contact_avatar_corner_radius_percent);
+    }
+
+    /**
+     * Removes all messages related to the inputted predicate, and cancels their notifications.
+     **/
+    public void cleanupMessagesAndNotifications(Predicate<CompositeKey> predicate) {
+        clearNotifications(predicate);
+        mNotificationBuilders.entrySet().removeIf(entry -> predicate.test(entry.getKey()));
+        mNotificationInfos.entrySet().removeIf(entry -> predicate.test(entry.getKey()));
+        mSenderLargeIcons.entrySet().removeIf(entry -> predicate.test(entry.getKey()));
+        mMessages.entrySet().removeIf(
+                messageKeyMapMessageEntry -> predicate.test(messageKeyMapMessageEntry.getKey()));
+    }
+
+    /**
+     * Clears all notifications matching the predicate. Example method calls are when user
+     * wants to clear (a) message notification(s), or when the Bluetooth device that received the
+     * messages has been disconnected.
+     */
+    public void clearNotifications(Predicate<CompositeKey> predicate) {
+        mNotificationInfos.forEach((conversationKey, notificationInfo) -> {
+            if (predicate.test(conversationKey)) {
+                mNotificationManager.cancel(notificationInfo.getNotificationId());
+            }
+        });
+    }
+
+    /**
+     * Helper method to add {@link Message}s to the {@link ConversationNotificationInfo}. This
+     * should be called when a new message has arrived.
+     **/
+    protected void addMessageToNotificationInfo(Message message, ConversationKey convoKey) {
+        MessageKey messageKey = new MessageKey(message);
+        boolean repeatMessage = mMessages.containsKey(messageKey);
+        mMessages.put(messageKey, message);
+        if (!repeatMessage) {
+            ConversationNotificationInfo notificationInfo = mNotificationInfos.get(convoKey);
+            notificationInfo.mMessageKeys.add(messageKey);
+        }
+    }
+
+    /**
+     * Creates a new notification, or updates an existing notification with the latest messages,
+     * then posts it.
+     * This should be called after the {@link ConversationNotificationInfo} object has been created,
+     * and all of its {@link Message} objects have been linked to it.
+     **/
+    protected void postNotification(ConversationKey conversationKey,
+            ConversationNotificationInfo notificationInfo, String channelId) {
+        boolean newNotification = !mNotificationBuilders.containsKey(conversationKey);
+
+        NotificationCompat.Builder builder = newNotification ? new NotificationCompat.Builder(
+                mContext, channelId) : mNotificationBuilders.get(
+                conversationKey);
+        builder.setChannelId(channelId);
+        Message lastMessage = mMessages.get(notificationInfo.mMessageKeys.getLast());
+
+        builder.setContentTitle(notificationInfo.getConvoTitle());
+        builder.setContentText(mContext.getResources().getQuantityString(
+                R.plurals.notification_new_message, notificationInfo.mMessageKeys.size(),
+                notificationInfo.mMessageKeys.size()));
+
+        if (mSenderLargeIcons.containsKey(getSenderKeyFromConversation(conversationKey))) {
+            builder.setLargeIcon(
+                    mSenderLargeIcons.get(getSenderKeyFromConversation(conversationKey)));
+        } else if (mUseLetterTile) {
+            builder.setLargeIcon(Utils.createLetterTile(mContext,
+                    Utils.getInitials(lastMessage.getSenderName(), ""),
+                    lastMessage.getSenderName(), mBitmapSize, mCornerRadiusPercent));
+        }
+        // Else, no avatar icon will be shown.
+
+        builder.setWhen(lastMessage.getReceiveTime());
+
+        // Create MessagingStyle
+        String userName = (notificationInfo.getUserDisplayName() == null
+                || notificationInfo.getUserDisplayName().isEmpty()) ? mContext.getString(
+                R.string.name_not_available) : notificationInfo.getUserDisplayName();
+        Person user = new Person.Builder()
+                .setName(userName)
+                .build();
+        NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(
+                user);
+        Person sender = new Person.Builder()
+                .setName(lastMessage.getSenderName())
+                .setUri(lastMessage.getSenderContactUri())
+                .build();
+        notificationInfo.mMessageKeys.stream().map(mMessages::get).forEachOrdered(message -> {
+            if (!message.shouldExcludeFromNotification()) {
+                messagingStyle.addMessage(
+                        message.getMessageText(),
+                        message.getReceiveTime(),
+                        notificationInfo.isGroupConvo() ? new Person.Builder()
+                                .setName(message.getSenderName())
+                                .setUri(message.getSenderContactUri())
+                                .build() : sender);
+            }
+        });
+        if (notificationInfo.isGroupConvo()) {
+            messagingStyle.setConversationTitle(
+                    mContext.getString(R.string.group_conversation_title_separator,
+                            lastMessage.getSenderName(), notificationInfo.getConvoTitle()));
+        }
+
+        // We are creating this notification for the first time.
+        if (newNotification) {
+            builder.setCategory(Notification.CATEGORY_MESSAGE);
+            if (notificationInfo.getAppSmallIconResId() == 0) {
+                builder.setSmallIcon(R.drawable.ic_message);
+            } else {
+                builder.setSmallIcon(notificationInfo.getAppSmallIconResId());
+            }
+
+            builder.setShowWhen(true);
+            messagingStyle.setGroupConversation(notificationInfo.isGroupConvo());
+
+            if (notificationInfo.getAppDisplayName() != null) {
+                Bundle displayName = new Bundle();
+                displayName.putCharSequence(Notification.EXTRA_SUBSTITUTE_APP_NAME,
+                        notificationInfo.getAppDisplayName());
+                builder.addExtras(displayName);
+            }
+
+            PendingIntent deleteIntent = createServiceIntent(conversationKey,
+                    notificationInfo.getNotificationId(),
+                    ACTION_DISMISS_NOTIFICATION);
+            builder.setDeleteIntent(deleteIntent);
+
+            List<Action> actions = buildNotificationActions(conversationKey,
+                    notificationInfo.getNotificationId());
+            for (final Action action : actions) {
+                builder.addAction(action);
+            }
+        }
+        builder.setStyle(messagingStyle);
+
+        mNotificationBuilders.put(conversationKey, builder);
+        mNotificationManager.notify(notificationInfo.getNotificationId(), builder.build());
+    }
+
+    /** Can be overridden by any Delegates that have some devices that do not support reply. **/
+    protected boolean shouldAddReplyAction(String deviceAddress) {
+        return true;
+    }
+
+    private List<Action> buildNotificationActions(ConversationKey conversationKey,
+            int notificationId) {
+        final int icon = android.R.drawable.ic_media_play;
+
+        final List<NotificationCompat.Action> actionList = new ArrayList<>();
+
+        // Reply action
+        if (shouldAddReplyAction(conversationKey.getDeviceId())) {
+            final String replyString = mContext.getString(R.string.action_reply);
+            PendingIntent replyIntent = createServiceIntent(conversationKey, notificationId,
+                    ACTION_REPLY);
+            actionList.add(
+                    new NotificationCompat.Action.Builder(icon, replyString, replyIntent)
+                            .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
+                            .setShowsUserInterface(false)
+                            .addRemoteInput(
+                                    new androidx.core.app.RemoteInput.Builder(
+                                            EXTRA_REMOTE_INPUT_KEY)
+                                            .build()
+                            )
+                            .build()
+            );
+        }
+
+        // Mark-as-read Action. This will be the callback of Notification Center's "Read" action.
+        final String markAsRead = mContext.getString(R.string.action_mark_as_read);
+        PendingIntent markAsReadIntent = createServiceIntent(conversationKey, notificationId,
+                ACTION_MARK_AS_READ);
+        actionList.add(
+                new NotificationCompat.Action.Builder(icon, markAsRead, markAsReadIntent)
+                        .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
+                        .setShowsUserInterface(false)
+                        .build()
+        );
+
+        return actionList;
+    }
+
+    private PendingIntent createServiceIntent(ConversationKey conversationKey, int notificationId,
+            String action) {
+        Intent intent = new Intent(mContext, mContext.getClass())
+                .setAction(action)
+                .setClassName(mContext, mClassName)
+                .putExtra(EXTRA_CONVERSATION_KEY, conversationKey);
+
+        return PendingIntent.getForegroundService(mContext, notificationId, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    protected SenderKey getSenderKeyFromConversation(ConversationKey conversationKey) {
+        ConversationNotificationInfo info = mNotificationInfos.get(conversationKey);
+        return mMessages.get(info.getLastMessageKey()).getSenderKey();
+    }
+
+}
diff --git a/car-messenger-common/src/com/android/car/messenger/common/CompositeKey.java b/car-messenger-common/src/com/android/car/messenger/common/CompositeKey.java
new file mode 100644
index 0000000..4d8bacd
--- /dev/null
+++ b/car-messenger-common/src/com/android/car/messenger/common/CompositeKey.java
@@ -0,0 +1,80 @@
+/*
+ * 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.messenger.common;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A composite key used for {@link Map} lookups, using two strings for
+ * checking equality and hashing.
+ */
+public abstract class CompositeKey {
+    private final String mDeviceId;
+    private final String mSubKey;
+
+    protected CompositeKey(String deviceId, String subKey) {
+        mDeviceId = deviceId;
+        mSubKey = subKey;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof CompositeKey)) {
+            return false;
+        }
+
+        CompositeKey that = (CompositeKey) o;
+        return Objects.equals(mDeviceId, that.mDeviceId)
+                && Objects.equals(mSubKey, that.mSubKey);
+    }
+
+    /**
+     * Returns true if the device address of this composite key equals {@code deviceId}.
+     *
+     * @param deviceId the device address which is compared to this key's device address
+     * @return true if the device addresses match
+     */
+    public boolean matches(String deviceId) {
+        return mDeviceId.equals(deviceId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDeviceId, mSubKey);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s, deviceId: %s, subKey: %s",
+                getClass().getSimpleName(), mDeviceId, mSubKey);
+    }
+
+    /** Returns this composite key's device address. */
+    public String getDeviceId() {
+        return mDeviceId;
+    }
+
+    /** Returns this composite key's sub key. */
+    public String getSubKey() {
+        return mSubKey;
+    }
+}
diff --git a/car-messenger-common/src/com/android/car/messenger/common/ConversationKey.java b/car-messenger-common/src/com/android/car/messenger/common/ConversationKey.java
new file mode 100644
index 0000000..1b9b7b9
--- /dev/null
+++ b/car-messenger-common/src/com/android/car/messenger/common/ConversationKey.java
@@ -0,0 +1,57 @@
+/*
+ * 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.messenger.common;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * {@link CompositeKey} subclass used to give each conversation on all the connected devices a
+ * unique Key.
+ */
+public class ConversationKey extends CompositeKey implements Parcelable {
+
+    public ConversationKey(String deviceId, String key) {
+        super(deviceId, key);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(getDeviceId());
+        dest.writeString(getSubKey());
+    }
+
+    /** Creates {@link ConversationKey} instances from {@link Parcel} sources. */
+    public static final Parcelable.Creator<ConversationKey> CREATOR =
+            new Parcelable.Creator<ConversationKey>() {
+                @Override
+                public ConversationKey createFromParcel(Parcel source) {
+                    return new ConversationKey(source.readString(), source.readString());
+                }
+
+                @Override
+                public ConversationKey[] newArray(int size) {
+                    return new ConversationKey[size];
+                }
+            };
+
+}
diff --git a/car-messenger-common/src/com/android/car/messenger/common/ConversationNotificationInfo.java b/car-messenger-common/src/com/android/car/messenger/common/ConversationNotificationInfo.java
new file mode 100644
index 0000000..5567f50
--- /dev/null
+++ b/car-messenger-common/src/com/android/car/messenger/common/ConversationNotificationInfo.java
@@ -0,0 +1,178 @@
+/*
+ * 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.messenger.common;
+
+import static com.android.car.apps.common.util.SafeLog.logw;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ConversationNotification;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyle;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.PhoneToCarMessage;
+
+import java.util.LinkedList;
+
+/**
+ * Represents a conversation notification's metadata that is shared between the conversation's
+ * messages. Note, each {@link ConversationKey} should map to exactly one
+ * ConversationNotificationInfo object.
+ **/
+public class ConversationNotificationInfo {
+    private static final String TAG = "CMC.ConversationNotificationInfo";
+    private static int sNextNotificationId = 0;
+    final int mNotificationId = sNextNotificationId++;
+
+    private final String mDeviceName;
+    private final String mDeviceId;
+    // This is always the sender name for SMS Messages from Bluetooth MAP.
+    private final String mConvoTitle;
+    private final boolean mIsGroupConvo;
+
+    /** Only used for {@link NotificationMsg} conversations. **/
+    @Nullable
+    private final String mNotificationKey;
+    @Nullable
+    private final String mAppDisplayName;
+    @Nullable
+    private final String mUserDisplayName;
+    private final int mAppSmallIconResId;
+
+    public final LinkedList<MessageKey> mMessageKeys = new LinkedList<>();
+
+    /**
+     * Creates a ConversationNotificationInfo for a {@link NotificationMsg}. Returns {@code null} if
+     * the {@link ConversationNotification} is missing required fields.
+     **/
+    @Nullable
+    public static ConversationNotificationInfo createConversationNotificationInfo(
+            @NonNull String deviceName, @NonNull String deviceId,
+            @NonNull ConversationNotification conversation, @NonNull String notificationKey) {
+        MessagingStyle messagingStyle = conversation.getMessagingStyle();
+
+        if (!Utils.isValidConversationNotification(conversation, /* isShallowCheck= */ true)) {
+            if (Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE) {
+                throw new IllegalArgumentException(
+                        "ConversationNotificationInfo is missing required fields");
+            } else {
+                logw(TAG, "ConversationNotificationInfo is missing required fields");
+                return null;
+            }
+        }
+
+        return new ConversationNotificationInfo(deviceName, deviceId,
+                messagingStyle.getConvoTitle(),
+                messagingStyle.getIsGroupConvo(), notificationKey,
+                conversation.getMessagingAppDisplayName(),
+                messagingStyle.getUserDisplayName(), /* appSmallIconResId= */ 0);
+
+    }
+
+    private ConversationNotificationInfo(@Nullable String deviceName, String deviceId,
+            String convoTitle, boolean isGroupConvo, @Nullable String notificationKey,
+            @Nullable String appDisplayName, @Nullable String userDisplayName,
+            int appSmallIconResId) {
+        boolean missingDeviceId = (deviceId == null);
+        boolean missingTitle = (convoTitle == null);
+        if (missingDeviceId || missingTitle) {
+            StringBuilder builder = new StringBuilder("Missing required fields:");
+            if (missingDeviceId) {
+                builder.append(" deviceId");
+            }
+            if (missingTitle) {
+                builder.append(" convoTitle");
+            }
+            throw new IllegalArgumentException(builder.toString());
+        }
+        this.mDeviceName = deviceName;
+        this.mDeviceId = deviceId;
+        this.mConvoTitle = convoTitle;
+        this.mIsGroupConvo = isGroupConvo;
+        this.mNotificationKey = notificationKey;
+        this.mAppDisplayName = appDisplayName;
+        this.mUserDisplayName = userDisplayName;
+        this.mAppSmallIconResId = appSmallIconResId;
+    }
+
+    /** Returns the id that should be used for this object's {@link android.app.Notification} **/
+    public int getNotificationId() {
+        return mNotificationId;
+    }
+
+    /** Returns the friendly name of the device that received the notification. **/
+    public String getDeviceName() {
+        return mDeviceName;
+    }
+
+    /** Returns the address of the device that received the notification. **/
+    public String getDeviceId() {
+        return mDeviceId;
+    }
+
+    /**
+     * Returns the conversation title of this notification. If this notification came from MAP
+     * profile, the title will be the Sender's name.
+     */
+    public String getConvoTitle() {
+        return mConvoTitle;
+    }
+
+    /** Returns {@code true} if this message is in a group conversation **/
+    public boolean isGroupConvo() {
+        return mIsGroupConvo;
+    }
+
+    /**
+     * Returns the key if this conversation is based on a {@link ConversationNotification}. Refer to
+     * {@link PhoneToCarMessage#getNotificationKey()} for more info.
+     */
+    @Nullable
+    public String getNotificationKey() {
+        return mNotificationKey;
+    }
+
+    /**
+     * Returns the display name of the application that posted this notification if this object is
+     * based on a {@link ConversationNotification}.
+     **/
+    @Nullable
+    public String getAppDisplayName() {
+        return mAppDisplayName;
+    }
+
+    /**
+     * Returns the User Display Name if this object is based on a @link ConversationNotification}.
+     * This is needed for {@link android.app.Notification.MessagingStyle}.
+     */
+    @Nullable
+    public String getUserDisplayName() {
+        return mUserDisplayName;
+    }
+
+
+    /** Returns the icon's resource id of the application that posted this notification. **/
+    public int getAppSmallIconResId() {
+        return mAppSmallIconResId;
+    }
+
+    public MessageKey getLastMessageKey() {
+        return mMessageKeys.getLast();
+    }
+}
diff --git a/car-messenger-common/src/com/android/car/messenger/common/Message.java b/car-messenger-common/src/com/android/car/messenger/common/Message.java
new file mode 100644
index 0000000..017d055
--- /dev/null
+++ b/car-messenger-common/src/com/android/car/messenger/common/Message.java
@@ -0,0 +1,233 @@
+/*
+ * 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.messenger.common;
+
+import static com.android.car.apps.common.util.SafeLog.logw;
+
+import android.annotation.Nullable;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyleMessage;
+
+
+/**
+ * Represents a SMS, MMS, and {@link NotificationMsg}. This object is based
+ * on {@link NotificationMsg}.
+ */
+public class Message {
+    private static final String TAG = "CMC.Message";
+
+    private final String mSenderName;
+    private final String mDeviceId;
+    private final String mMessageText;
+    private final long mReceiveTime;
+    private final boolean mIsReadOnPhone;
+    private boolean mShouldExclude;
+    private final String mHandle;
+    private final MessageType mMessageType;
+    private final SenderKey mSenderKey;
+
+
+    /**
+     * Note: MAP messages from iOS version 12 and earlier, as well as {@link MessagingStyleMessage},
+     * don't provide these.
+     */
+    @Nullable
+    final String mSenderContactUri;
+
+    /**
+     * Describes if the message was received through Bluetooth MAP or is a {@link NotificationMsg}.
+     */
+    public enum MessageType {
+        BLUETOOTH_MAP_MESSAGE, NOTIFICATION_MESSAGE
+    }
+
+    /**
+     * Creates a Message based on {@link MessagingStyleMessage}. Returns {@code null} if the {@link
+     * MessagingStyleMessage} is missing required fields.
+     *
+     * @param deviceId of the phone that received this message.
+     * @param updatedMessage containing the information to base this message object off of.
+     * @param appDisplayName of the messaging app this message belongs to.
+     **/
+    @Nullable
+    public static Message parseFromMessage(String deviceId,
+            MessagingStyleMessage updatedMessage, String appDisplayName) {
+
+        if (!Utils.isValidMessagingStyleMessage(updatedMessage)) {
+            if (Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE) {
+                throw new IllegalArgumentException(
+                        "MessagingStyleMessage is missing required fields");
+            } else {
+                logw(TAG, "MessagingStyleMessage is missing required fields");
+                return null;
+            }
+        }
+
+        return new Message(updatedMessage.getSender().getName(),
+                deviceId,
+                updatedMessage.getTextMessage(),
+                updatedMessage.getTimestamp(),
+                updatedMessage.getIsRead(),
+                Utils.createMessageHandle(updatedMessage),
+                MessageType.NOTIFICATION_MESSAGE,
+                /* senderContactUri */ null,
+                appDisplayName);
+    }
+
+    private Message(String senderName, String deviceId, String messageText, long receiveTime,
+            boolean isReadOnPhone, String handle, MessageType messageType,
+            @Nullable String senderContactUri, String senderKeyMetadata) {
+        boolean missingSenderName = (senderName == null);
+        boolean missingDeviceId = (deviceId == null);
+        boolean missingText = (messageText == null);
+        boolean missingHandle = (handle == null);
+        boolean missingType = (messageType == null);
+        if (missingSenderName || missingDeviceId || missingText || missingHandle || missingType) {
+            StringBuilder builder = new StringBuilder("Missing required fields:");
+            if (missingSenderName) {
+                builder.append(" senderName");
+            }
+            if (missingDeviceId) {
+                builder.append(" deviceId");
+            }
+            if (missingText) {
+                builder.append(" messageText");
+            }
+            if (missingHandle) {
+                builder.append(" handle");
+            }
+            if (missingType) {
+                builder.append(" type");
+            }
+            throw new IllegalArgumentException(builder.toString());
+        }
+        this.mSenderName = senderName;
+        this.mDeviceId = deviceId;
+        this.mMessageText = messageText;
+        this.mReceiveTime = receiveTime;
+        this.mIsReadOnPhone = isReadOnPhone;
+        this.mShouldExclude = false;
+        this.mHandle = handle;
+        this.mMessageType = messageType;
+        this.mSenderContactUri = senderContactUri;
+        this.mSenderKey = new SenderKey(deviceId, senderName, senderKeyMetadata);
+    }
+
+    /**
+     * Returns the contact name as obtained from the device.
+     * If contact is in the device's address-book, this is typically the contact name.
+     * Otherwise it will be the phone number.
+     */
+    public String getSenderName() {
+        return mSenderName;
+    }
+
+    /**
+     * Returns the id of the device from which this message was received.
+     */
+    public String getDeviceId() {
+        return mDeviceId;
+    }
+
+    /**
+     * Returns the actual content of the message.
+     */
+    public String getMessageText() {
+        return mMessageText;
+    }
+
+    /**
+     * Returns the milliseconds since epoch at which this message notification was received on the
+     * head-unit.
+     */
+    public long getReceiveTime() {
+        return mReceiveTime;
+    }
+
+    /**
+     * Whether message should be included in the notification. Messages that have been read aloud on
+     * the car, or that have been dismissed by the user should be excluded from the notification if/
+     * when the notification gets updated. Note: this state will not be propagated to the phone.
+     */
+    public void excludeFromNotification() {
+        mShouldExclude = true;
+    }
+
+    /**
+     * Returns {@code true} if message was read on the phone before it was received on the car.
+     */
+    public boolean isReadOnPhone() {
+        return mIsReadOnPhone;
+    }
+
+    /**
+     * Returns {@code true} if message should not be included in the notification. Messages that
+     * have been read aloud on the car, or that have been dismissed by the user should be excluded
+     * from the notification if/when the notification gets updated.
+     */
+    public boolean shouldExcludeFromNotification() {
+        return mShouldExclude;
+    }
+
+    /**
+     * Returns a unique handle/key for this message. This is used as this Message's
+     * {@link MessageKey#getSubKey()} Note: this handle might only be unique for the lifetime of a
+     * device connection session.
+     */
+    public String getHandle() {
+        return mHandle;
+    }
+
+    /**
+     * Returns the {@link SenderKey} that is unique for each contact per device.
+     */
+    public SenderKey getSenderKey() {
+        return mSenderKey;
+    }
+
+    /** Returns whether the message is a SMS/MMS or a {@link NotificationMsg} **/
+    public MessageType getMessageType() {
+        return mMessageType;
+    }
+
+    /**
+     * Returns the sender's phone number available as a URI string.
+     * Note: MAP messages from iOS version 12 and earlier, as well as {@link MessagingStyleMessage},
+     * don't provide these.
+     */
+    @Nullable
+    public String getSenderContactUri() {
+        return mSenderContactUri;
+    }
+
+    @Override
+    public String toString() {
+        return "Message{"
+                + " mSenderName='" + mSenderName + '\''
+                + ", mMessageText='" + mMessageText + '\''
+                + ", mSenderContactUri='" + mSenderContactUri + '\''
+                + ", mReceiveTime=" + mReceiveTime + '\''
+                + ", mIsReadOnPhone= " + mIsReadOnPhone + '\''
+                + ", mShouldExclude= " + mShouldExclude + '\''
+                + ", mHandle='" + mHandle + '\''
+                + ", mSenderKey='" + mSenderKey.toString()
+                + "}";
+    }
+}
diff --git a/car-messenger-common/src/com/android/car/messenger/common/MessageKey.java b/car-messenger-common/src/com/android/car/messenger/common/MessageKey.java
new file mode 100644
index 0000000..8244197
--- /dev/null
+++ b/car-messenger-common/src/com/android/car/messenger/common/MessageKey.java
@@ -0,0 +1,29 @@
+/*
+ * 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.messenger.common;
+
+/**
+ * {@link CompositeKey} subclass used to give each message on all the connected devices a
+ * unique Key.
+ **/
+public class MessageKey extends CompositeKey {
+
+    /** Creates a MessageKey for a {@link Message}. **/
+    public MessageKey(Message message) {
+        super(message.getDeviceId(), message.getHandle());
+    }
+}
diff --git a/car-messenger-common/src/com/android/car/messenger/common/ProjectionStateListener.java b/car-messenger-common/src/com/android/car/messenger/common/ProjectionStateListener.java
new file mode 100644
index 0000000..5557830
--- /dev/null
+++ b/car-messenger-common/src/com/android/car/messenger/common/ProjectionStateListener.java
@@ -0,0 +1,143 @@
+/*
+ * 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.messenger.common;
+
+import static com.android.car.apps.common.util.SafeLog.logd;
+import static com.android.car.apps.common.util.SafeLog.loge;
+import static com.android.car.apps.common.util.SafeLog.logi;
+
+import android.bluetooth.BluetoothDevice;
+import android.car.Car;
+import android.car.CarProjectionManager;
+import android.car.projection.ProjectionStatus;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import androidx.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link ProjectionStatus} listener that exposes APIs to detect whether a projection application
+ * is active.
+ */
+public class ProjectionStateListener implements CarProjectionManager.ProjectionStatusListener{
+    private static final String TAG = "CMC.ProjectionStateHandler";
+    static final String PROJECTION_STATUS_EXTRA_DEVICE_STATE =
+            "android.car.projection.DEVICE_STATE";
+
+    private final CarProjectionManager mCarProjectionManager;
+
+    private int mProjectionState = ProjectionStatus.PROJECTION_STATE_INACTIVE;
+    private List<ProjectionStatus> mProjectionDetails = Collections.emptyList();
+
+    public ProjectionStateListener(Context context) {
+        mCarProjectionManager = (CarProjectionManager)
+                Car.createCar(context).getCarManager(Car.PROJECTION_SERVICE);
+    }
+
+    /** Registers the listener. Should be called when the caller starts up. **/
+    public void start() {
+        mCarProjectionManager.registerProjectionStatusListener(this);
+    }
+
+    /** Unregisters the listener. Should be called when the caller's lifecycle is ending. **/
+    public void stop() {
+        mCarProjectionManager.unregisterProjectionStatusListener(this);
+    }
+
+
+    @Override
+    public void onProjectionStatusChanged(int state, String packageName,
+            List<ProjectionStatus> details) {
+        mProjectionState = state;
+        mProjectionDetails = details;
+
+    }
+
+    /**
+     * Returns {@code true} if the input device currently has a projection app running in the
+     * foreground.
+     * @param bluetoothAddress of the device that should be checked. If null, return whether any
+     *                         device is currently running a projection app in the foreground.
+     */
+    public boolean isProjectionInActiveForeground(@Nullable String bluetoothAddress) {
+        if (bluetoothAddress == null) {
+            logi(TAG, "returning non-device-specific projection status");
+            return isProjectionInActiveForeground();
+        }
+
+        if (!isProjectionInActiveForeground()) {
+            return false;
+        }
+
+        for (ProjectionStatus status : mProjectionDetails) {
+            if (!status.isActive()) {
+                // Don't suppress UI for packages that aren't actively projecting.
+                logd(TAG, "skip non-projecting package " + status.getPackageName());
+                continue;
+            }
+
+            for (ProjectionStatus.MobileDevice device : status.getConnectedMobileDevices()) {
+                if (!device.isProjecting()) {
+                    // Don't suppress UI for devices that aren't foreground.
+                    logd(TAG, "skip non-projecting device " + device.getName());
+                    continue;
+                }
+
+                Bundle extras = device.getExtras();
+                if (extras.getInt(PROJECTION_STATUS_EXTRA_DEVICE_STATE,
+                        ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND)
+                        != ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND) {
+                    logd(TAG, "skip device " + device.getName() + " - not foreground");
+                    continue;
+                }
+
+                Parcelable projectingBluetoothDevice =
+                        extras.getParcelable(BluetoothDevice.EXTRA_DEVICE);
+                logd(TAG, "Device " + device.getName() + " has BT device "
+                        + projectingBluetoothDevice);
+
+                if (projectingBluetoothDevice == null) {
+                    logi(TAG, "Suppressing message notification - device " + device
+                            + " is projection, and does not specify a Bluetooth address");
+                    return true;
+                } else if (!(projectingBluetoothDevice instanceof BluetoothDevice)) {
+                    loge(TAG, "Device " + device + " has bad EXTRA_DEVICE value "
+                            + projectingBluetoothDevice + " - treating as unspecified");
+                    return true;
+                } else if (bluetoothAddress.equals(
+                        ((BluetoothDevice) projectingBluetoothDevice).getAddress())) {
+                    logi(TAG, "Suppressing message notification - device " + device
+                            + "is projecting, and message is coming from device's Bluetooth address"
+                            + bluetoothAddress);
+                    return true;
+                }
+            }
+        }
+
+        // No projecting apps want to suppress this device, so let it through.
+        return false;
+    }
+
+    /** Returns {@code true} if a projection app is active in the foreground. **/
+    private boolean isProjectionInActiveForeground() {
+        return mProjectionState == ProjectionStatus.PROJECTION_STATE_ACTIVE_FOREGROUND;
+    }
+}
diff --git a/car-messenger-common/src/com/android/car/messenger/common/SenderKey.java b/car-messenger-common/src/com/android/car/messenger/common/SenderKey.java
new file mode 100644
index 0000000..2fcd273
--- /dev/null
+++ b/car-messenger-common/src/com/android/car/messenger/common/SenderKey.java
@@ -0,0 +1,30 @@
+/*
+ * 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.messenger.common;
+
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
+
+/**
+ * {@link CompositeKey} subclass used to give each contact on all the connected devices a
+ * unique Key.
+ */
+public class SenderKey extends CompositeKey {
+    /** Creates a senderkey for SMS, MMS, and {@link NotificationMsg}. **/
+    protected SenderKey(String deviceId, String senderName, String keyMetadata) {
+        super(deviceId, senderName + "/" + keyMetadata);
+    }
+}
diff --git a/car-messenger-common/src/com/android/car/messenger/common/Utils.java b/car-messenger-common/src/com/android/car/messenger/common/Utils.java
new file mode 100644
index 0000000..027189e
--- /dev/null
+++ b/car-messenger-common/src/com/android/car/messenger/common/Utils.java
@@ -0,0 +1,229 @@
+/*
+ * 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.messenger.common;
+
+import static com.android.car.apps.common.util.SafeLog.logw;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
+import com.android.car.apps.common.LetterTileDrawable;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.ConversationNotification;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyle;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.MessagingStyleMessage;
+import com.android.car.messenger.NotificationMsgProto.NotificationMsg.Person;
+
+/** Utils methods for the car-messenger-common lib. **/
+public class Utils {
+    private static final String TAG = "CMC.Utils";
+    /**
+     * Represents the maximum length of a message substring to be used when constructing the
+     * message's unique handle/key.
+     */
+    private static final int MAX_SUB_MESSAGE_LENGTH = 5;
+
+    /** Gets the latest message for a {@link NotificationMsg} Conversation. **/
+    public static MessagingStyleMessage getLatestMessage(
+            ConversationNotification notification) {
+        MessagingStyle messagingStyle = notification.getMessagingStyle();
+        long latestTime = 0;
+        MessagingStyleMessage latestMessage = null;
+
+        for (MessagingStyleMessage message : messagingStyle.getMessagingStyleMsgList()) {
+            if (message.getTimestamp() > latestTime) {
+                latestTime = message.getTimestamp();
+                latestMessage = message;
+            }
+        }
+        return latestMessage;
+    }
+
+    /**
+     * Helper method to create a unique handle/key for this message. This is used as this Message's
+     * {@link MessageKey#getSubKey()}.
+     */
+    public static String createMessageHandle(MessagingStyleMessage message) {
+        String textMessage = message.getTextMessage();
+        String subMessage = textMessage.substring(
+                Math.min(MAX_SUB_MESSAGE_LENGTH, textMessage.length()));
+        return message.getTimestamp() + "/" + message.getSender().getName() + "/" + subMessage;
+    }
+
+    /**
+     * Ensure the {@link ConversationNotification} object has all the required fields.
+     *
+     * @param isShallowCheck should be {@code true} if the caller only wants to verify the
+     *                       notification and its {@link MessagingStyle} is valid, without checking
+     *                       all of the notification's {@link MessagingStyleMessage}s.
+     **/
+    public static boolean isValidConversationNotification(ConversationNotification notification,
+            boolean isShallowCheck) {
+        if (notification == null) {
+            logw(TAG, "ConversationNotification is null");
+            return false;
+        } else if (!notification.hasMessagingStyle()) {
+            logw(TAG, "ConversationNotification is missing required field: messagingStyle");
+            return false;
+        } else if (notification.getMessagingAppDisplayName() == null) {
+            logw(TAG, "ConversationNotification is missing required field: appDisplayName");
+            return false;
+        } else if (notification.getMessagingAppPackageName() == null) {
+            logw(TAG, "ConversationNotification is missing required field: appPackageName");
+            return false;
+        }
+        return isValidMessagingStyle(notification.getMessagingStyle(), isShallowCheck);
+    }
+
+    /**
+     * Ensure the {@link MessagingStyle} object has all the required fields.
+     **/
+    private static boolean isValidMessagingStyle(MessagingStyle messagingStyle,
+            boolean isShallowCheck) {
+        if (messagingStyle == null) {
+            logw(TAG, "MessagingStyle is null");
+            return false;
+        } else if (messagingStyle.getConvoTitle() == null) {
+            logw(TAG, "MessagingStyle is missing required field: convoTitle");
+            return false;
+        } else if (messagingStyle.getUserDisplayName() == null) {
+            logw(TAG, "MessagingStyle is missing required field: userDisplayName");
+            return false;
+        } else if (messagingStyle.getMessagingStyleMsgCount() == 0) {
+            logw(TAG, "MessagingStyle is missing required field: messagingStyleMsg");
+            return false;
+        }
+        if (!isShallowCheck) {
+            for (MessagingStyleMessage message : messagingStyle.getMessagingStyleMsgList()) {
+                if (!isValidMessagingStyleMessage(message)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Ensure the {@link MessagingStyleMessage} object has all the required fields.
+     **/
+    public static boolean isValidMessagingStyleMessage(MessagingStyleMessage message) {
+        if (message == null) {
+            logw(TAG, "MessagingStyleMessage is null");
+            return false;
+        } else if (message.getTextMessage() == null) {
+            logw(TAG, "MessagingStyleMessage is missing required field: textMessage");
+            return false;
+        } else if (!message.hasSender()) {
+            logw(TAG, "MessagingStyleMessage is missing required field: sender");
+            return false;
+        }
+        return isValidSender(message.getSender());
+    }
+
+    /**
+     * Ensure the {@link Person} object has all the required fields.
+     **/
+    public static boolean isValidSender(Person person) {
+        if (person.getName() == null) {
+            logw(TAG, "Person is missing required field: name");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Creates a Letter Tile Icon that will display the given initials. If the initials are null,
+     * then an avatar anonymous icon will be drawn.
+     **/
+    public static Bitmap createLetterTile(Context context, @Nullable String initials,
+            String identifier, int avatarSize, float cornerRadiusPercent) {
+        // TODO(b/135446418): use TelecomUtils once car-telephony-common supports bp.
+        LetterTileDrawable letterTileDrawable = createLetterTileDrawable(context, initials,
+                identifier);
+        RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(
+                context.getResources(), letterTileDrawable.toBitmap(avatarSize));
+        return createFromRoundedBitmapDrawable(roundedBitmapDrawable, avatarSize,
+                cornerRadiusPercent);
+    }
+
+    /** Creates an Icon based on the given roundedBitmapDrawable. **/
+    private static Bitmap createFromRoundedBitmapDrawable(
+            RoundedBitmapDrawable roundedBitmapDrawable, int avatarSize,
+            float cornerRadiusPercent) {
+        // TODO(b/135446418): use TelecomUtils once car-telephony-common supports bp.
+        float radius = avatarSize * cornerRadiusPercent;
+        roundedBitmapDrawable.setCornerRadius(radius);
+
+        final Bitmap result = Bitmap.createBitmap(avatarSize, avatarSize,
+                Bitmap.Config.ARGB_8888);
+        final Canvas canvas = new Canvas(result);
+        roundedBitmapDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        roundedBitmapDrawable.draw(canvas);
+        return roundedBitmapDrawable.getBitmap();
+    }
+
+
+    /**
+     * Create a {@link LetterTileDrawable} for the given initials.
+     *
+     * @param initials   is the letters that will be drawn on the canvas. If it is null, then an
+     *                   avatar anonymous icon will be drawn
+     * @param identifier will decide the color for the drawable. If null, a default color will be
+     *                   used.
+     */
+    private static LetterTileDrawable createLetterTileDrawable(
+            Context context,
+            @Nullable String initials,
+            @Nullable String identifier) {
+        // TODO(b/135446418): use TelecomUtils once car-telephony-common supports bp.
+        int numberOfLetter = context.getResources().getInteger(
+                R.integer.config_number_of_letters_shown_for_avatar);
+        String letters = initials != null
+                ? initials.substring(0, Math.min(initials.length(), numberOfLetter)) : null;
+        LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources(),
+                letters, identifier);
+        return letterTileDrawable;
+    }
+
+
+    /**
+     * Returns the initials based on the name and nameAlt.
+     *
+     * @param name    should be the display name of a contact.
+     * @param nameAlt should be alternative display name of a contact.
+     */
+    public static String getInitials(String name, String nameAlt) {
+        // TODO(b/135446418): use TelecomUtils once car-telephony-common supports bp.
+        StringBuilder initials = new StringBuilder();
+        if (!TextUtils.isEmpty(name) && Character.isLetter(name.charAt(0))) {
+            initials.append(Character.toUpperCase(name.charAt(0)));
+        }
+        if (!TextUtils.isEmpty(nameAlt)
+                && !TextUtils.equals(name, nameAlt)
+                && Character.isLetter(nameAlt.charAt(0))) {
+            initials.append(Character.toUpperCase(nameAlt.charAt(0)));
+        }
+        return initials.toString();
+    }
+
+}
diff --git a/car-telephony-common/src/com/android/car/telephony/common/Contact.java b/car-telephony-common/src/com/android/car/telephony/common/Contact.java
index a7fff6f..42a1b9a 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/Contact.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/Contact.java
@@ -227,7 +227,6 @@
         String lookupKey = cursor.getString(lookupKeyColumn);
 
         if (contact == null) {
-            Log.d(TAG, "A new contact will be created.");
             contact = new Contact();
             contact.loadBasicInfo(cursor);
         }
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 501fa02..960714a 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
@@ -233,6 +233,7 @@
         mLookupKeyContactMap.clear();
         mLookupKeyContactMap.putAll(contactMap);
 
+        mPhoneNumberContactMap.clear();
         for (Contact contact : contactList) {
             for (PhoneNumber phoneNumber : contact.getNumbers()) {
                 mPhoneNumberContactMap.put(phoneNumber.getI18nPhoneNumberWrapper(), contact);
diff --git a/car-ui-lib/Android.bp b/car-ui-lib/Android.bp
new file mode 100644
index 0000000..f433c4c
--- /dev/null
+++ b/car-ui-lib/Android.bp
@@ -0,0 +1,40 @@
+
+//
+// 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: "car-ui-lib-bp",
+
+    srcs: ["src/**/*.java"],
+
+    resource_dirs: ["res"],
+
+    optimize: {
+        enabled: false,
+    },
+
+    libs: ["android.car"],
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.appcompat_appcompat",
+	"androidx.asynclayoutinflater_asynclayoutinflater",
+        "androidx-constraintlayout_constraintlayout",
+        "androidx.preference_preference",
+        "androidx.recyclerview_recyclerview",
+        "androidx-constraintlayout_constraintlayout-solver",
+    ],
+}
diff --git a/car-ui-lib/build.gradle b/car-ui-lib/build.gradle
index aa720d4..7775382 100644
--- a/car-ui-lib/build.gradle
+++ b/car-ui-lib/build.gradle
@@ -35,6 +35,10 @@
         google()
         jcenter()
     }
+    buildDir = "/tmp/car-ui-build/${rootProject.name}/${project.name}"
+    tasks.withType(JavaCompile) {
+        options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+    }
 }
 
 // Library-level build file
@@ -71,5 +75,7 @@
     api 'androidx.constraintlayout:constraintlayout:1.1.3'
     api 'androidx.preference:preference:1.1.0'
     api 'androidx.recyclerview:recyclerview:1.0.0'
+
+    // This is the gradle equivalent of the libs: ["android.car"] in our Android.bp
     implementation files('../../../../../out/target/common/obj/JAVA_LIBRARIES/android.car_intermediates/classes.jar')
 }
diff --git a/car-ui-lib/findviewbyid-preupload-hook.sh b/car-ui-lib/findviewbyid-preupload-hook.sh
new file mode 100755
index 0000000..4969536
--- /dev/null
+++ b/car-ui-lib/findviewbyid-preupload-hook.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+if grep -rq "findViewById\|requireViewById" car-ui-lib/src/com/android/car/ui/toolbar/; then
+    grep -r "findViewById\|requireViewById" car-ui-lib/src/com/android/car/ui/toolbar/;
+    echo "Illegal use of findViewById or requireViewById in car-ui-lib. Please consider using CarUiUtils#findViewByRefId or CarUiUtils#requireViewByRefId" && false;
+fi
+
+if grep -rq "findViewById\|requireViewById" car-ui-lib/src/com/android/car/ui/recyclerview/; then
+    grep -r "findViewById\|requireViewById" car-ui-lib/src/com/android/car/ui/recyclerview/;
+    echo "Illegal use of findViewById or requireViewById in car-ui-lib. Please consider using CarUiUtils#findViewByRefId or CarUiUtils#requireViewByRefId" && false;
+fi
diff --git a/car-ui-lib/res/drawable/car_ui_icon_arrow_back.xml b/car-ui-lib/res/drawable/car_ui_icon_arrow_back.xml
index fcbba7c..4ad49b2 100644
--- a/car-ui-lib/res/drawable/car_ui_icon_arrow_back.xml
+++ b/car-ui-lib/res/drawable/car_ui_icon_arrow_back.xml
@@ -15,6 +15,7 @@
   limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:autoMirrored="true"
         android:width="24dp"
         android:height="24dp"
         android:viewportWidth="24.0"
diff --git a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml b/car-ui-lib/res/drawable/car_ui_list_header_background.xml
similarity index 60%
copy from car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
copy to car-ui-lib/res/drawable/car_ui_list_header_background.xml
index 19a1111..656b191 100644
--- a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
+++ b/car-ui-lib/res/drawable/car_ui_list_header_background.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ 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.
@@ -14,12 +14,6 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <com.android.car.ui.toolbar.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-</FrameLayout>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@android:color/transparent"/>
+</shape>
diff --git a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml b/car-ui-lib/res/drawable/car_ui_list_item_avatar_icon_outline.xml
similarity index 60%
copy from car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
copy to car-ui-lib/res/drawable/car_ui_list_item_avatar_icon_outline.xml
index 19a1111..f8b63f5 100644
--- a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
+++ b/car-ui-lib/res/drawable/car_ui_list_item_avatar_icon_outline.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ 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.
@@ -14,12 +14,6 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <com.android.car.ui.toolbar.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-</FrameLayout>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+</shape>
diff --git a/car-ui-lib/res/drawable/car_ui_list_item_background.xml b/car-ui-lib/res/drawable/car_ui_list_item_background.xml
new file mode 100644
index 0000000..df8df2f
--- /dev/null
+++ b/car-ui-lib/res/drawable/car_ui_list_item_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <solid android:color="?android:colorAccent" />
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml b/car-ui-lib/res/drawable/car_ui_preference_icon_chevron.xml
similarity index 60%
copy from car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
copy to car-ui-lib/res/drawable/car_ui_preference_icon_chevron.xml
index 19a1111..61d594c 100644
--- a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
+++ b/car-ui-lib/res/drawable/car_ui_preference_icon_chevron.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ 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.
@@ -14,12 +14,8 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <com.android.car.ui.toolbar.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-</FrameLayout>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:state_enabled="false" android:drawable="@drawable/car_ui_preference_icon_chevron_disabled"/>
+  <item android:state_enabled="true" android:drawable="@drawable/car_ui_preference_icon_chevron_enabled"/>
+</selector>
\ No newline at end of file
diff --git a/car-ui-lib/res/drawable/car_ui_recyclerview_divider.xml b/car-ui-lib/res/drawable/car_ui_recyclerview_divider.xml
index 7683148..e1c5163 100644
--- a/car-ui-lib/res/drawable/car_ui_recyclerview_divider.xml
+++ b/car-ui-lib/res/drawable/car_ui_recyclerview_divider.xml
@@ -15,8 +15,11 @@
   ~ limitations under the License.
   -->
 
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-       android:shape="rectangle">
-    <size android:height="@dimen/car_ui_recyclerview_divider_height" />
-    <solid android:color="@color/car_ui_recyclerview_divider_color" />
-</shape>
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:insetLeft="@dimen/car_ui_recyclerview_divider_start_margin"
+    android:insetRight="@dimen/car_ui_recyclerview_divider_end_margin">
+    <shape android:shape="rectangle">
+        <size android:height="@dimen/car_ui_recyclerview_divider_height" />
+        <solid android:color="@color/car_ui_recyclerview_divider_color" />
+    </shape>
+</inset>
diff --git a/car-ui-lib/res/layout-port/car_ui_base_layout_toolbar.xml b/car-ui-lib/res/layout-port/car_ui_base_layout_toolbar.xml
new file mode 100644
index 0000000..57d08ce
--- /dev/null
+++ b/car-ui-lib/res/layout-port/car_ui_base_layout_toolbar.xml
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<!-- This is for the two-row version of the toolbar -->
+<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="match_parent">
+
+    <!-- When the user finishes searching, we call clearFocus() on the editText in the search bar.
+     clearFocus() will actually send the focus to the first focusable thing in the layout.
+     If that focusable thing is still the search bar it will just reselect it, and the user won't
+     be able to deselect. So make a focusable view here to guarantee that we can clear the focus -->
+    <View
+        android:layout_width="1dp"
+        android:layout_height="1dp"
+        android:focusable="true"
+        android:focusableInTouchMode="true" />
+
+    <FrameLayout
+        android:id="@+id/content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/car_ui_toolbar_background"
+        style="@style/Widget.CarUi.Toolbar.Container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:tag="car_ui_top_inset"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/car_ui_toolbar_start_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_start_inset" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/car_ui_toolbar_top_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_top_inset" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/car_ui_toolbar_end_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_end="@dimen/car_ui_toolbar_end_inset" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/car_ui_toolbar_bottom_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            app:layout_constraintGuide_end="@dimen/car_ui_toolbar_bottom_inset" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/car_ui_toolbar_row_separator_guideline"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:orientation="horizontal"
+            app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_first_row_height" />
+
+        <View
+            android:id="@+id/car_ui_toolbar_row_separator"
+            style="@style/Widget.CarUi.Toolbar.SeparatorView"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/car_ui_toolbar_separator_height"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/car_ui_toolbar_row_separator_guideline" />
+
+        <FrameLayout
+            android:id="@+id/car_ui_toolbar_nav_icon_container"
+            style="@style/Widget.CarUi.Toolbar.NavIconContainer"
+            android:layout_width="@dimen/car_ui_toolbar_margin"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+            app:layout_constraintStart_toStartOf="@id/car_ui_toolbar_start_guideline"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline">
+
+            <ImageView
+                android:id="@+id/car_ui_toolbar_nav_icon"
+                style="@style/Widget.CarUi.Toolbar.NavIcon"
+                android:layout_width="@dimen/car_ui_toolbar_nav_icon_size"
+                android:layout_height="@dimen/car_ui_toolbar_nav_icon_size"
+                android:layout_gravity="center"
+                android:scaleType="fitXY" />
+
+            <ImageView
+                android:id="@+id/car_ui_toolbar_logo"
+                android:layout_width="@dimen/car_ui_toolbar_logo_size"
+                android:layout_height="@dimen/car_ui_toolbar_logo_size"
+                android:layout_gravity="center"
+                android:scaleType="fitXY" />
+        </FrameLayout>
+
+        <FrameLayout
+            android:id="@+id/car_ui_toolbar_title_logo_container"
+            style="@style/Widget.CarUi.Toolbar.LogoContainer"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+            app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_nav_icon_container"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline">
+
+            <ImageView
+                android:id="@+id/car_ui_toolbar_title_logo"
+                style="@style/Widget.CarUi.Toolbar.Logo"
+                android:layout_width="@dimen/car_ui_toolbar_logo_size"
+                android:layout_height="@dimen/car_ui_toolbar_logo_size"
+                android:layout_gravity="center"
+                android:scaleType="fitXY" />
+        </FrameLayout>
+
+        <TextView
+            android:id="@+id/car_ui_toolbar_title"
+            style="@style/Widget.CarUi.Toolbar.Title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+            app:layout_constraintEnd_toStartOf="@id/car_ui_toolbar_menu_items_container"
+            app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_title_logo_container"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline" />
+
+        <FrameLayout
+            android:id="@+id/car_ui_toolbar_search_view_container"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/car_ui_toolbar_search_height"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+            app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"
+            app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_nav_icon_container"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline" />
+
+        <LinearLayout
+            android:id="@+id/car_ui_toolbar_menu_items_container"
+            style="@style/Widget.CarUi.Toolbar.MenuItem.Container"
+            android:layout_width="wrap_content"
+            android:layout_height="0dp"
+            android:orientation="horizontal"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+            app:layout_constraintEnd_toStartOf="@id/car_ui_toolbar_end_guideline"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline" />
+
+        <com.android.car.ui.toolbar.TabLayout
+            android:id="@+id/car_ui_toolbar_tabs"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/car_ui_toolbar_second_row_height"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+            app:layout_constraintTop_toBottomOf="@id/car_ui_toolbar_row_separator" />
+
+        <View
+            android:id="@+id/car_ui_toolbar_bottom_styleable"
+            style="@style/Widget.CarUi.Toolbar.BottomView"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/car_ui_toolbar_bottom_view_height"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+        <ProgressBar
+            android:id="@+id/car_ui_toolbar_progress_bar"
+            style="@style/Widget.CarUi.Toolbar.ProgressBar"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:indeterminate="true"
+            android:visibility="gone"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_styleable"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
+
diff --git a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml b/car-ui-lib/res/layout/car_ui_alert_dialog_list.xml
similarity index 74%
rename from car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
rename to car-ui-lib/res/layout/car_ui_alert_dialog_list.xml
index 19a1111..34c7dbd 100644
--- a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
+++ b/car-ui-lib/res/layout/car_ui_alert_dialog_list.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ Copyright 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.
@@ -14,12 +14,10 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
+
+<androidx.recyclerview.widget.RecyclerView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/list"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
-    <com.android.car.ui.toolbar.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-</FrameLayout>
+</androidx.recyclerview.widget.RecyclerView>
diff --git a/car-ui-lib/res/layout/car_ui_alert_dialog_title_with_subtitle.xml b/car-ui-lib/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
index 389e511..271280d 100644
--- a/car-ui-lib/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
+++ b/car-ui-lib/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
@@ -32,10 +32,9 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         style="@style/Widget.CarUi.AlertDialog.TitleContainer">
-        <com.android.internal.widget.DialogTitle
+        <TextView
             android:id="@+id/alertTitle"
             android:singleLine="true"
-            android:ellipsize="end"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:textAlignment="viewStart"
diff --git a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml b/car-ui-lib/res/layout/car_ui_base_layout.xml
similarity index 73%
copy from car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
copy to car-ui-lib/res/layout/car_ui_base_layout.xml
index 19a1111..4cf7e8d 100644
--- a/car-ui-lib/tests/robotests/res/layout/test_toolbar.xml
+++ b/car-ui-lib/res/layout/car_ui_base_layout.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2019 The Android Open Source Project
+  ~ 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.
@@ -17,9 +17,6 @@
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <com.android.car.ui.toolbar.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-</FrameLayout>
+    android:layout_height="match_parent"
+    android:id="@+id/content">
+</FrameLayout>
\ No newline at end of file
diff --git a/car-ui-lib/res/layout/car_ui_base_layout_toolbar.xml b/car-ui-lib/res/layout/car_ui_base_layout_toolbar.xml
new file mode 100644
index 0000000..90b083a
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_base_layout_toolbar.xml
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<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="match_parent">
+
+    <!-- When the user finishes searching, we call clearFocus() on the editText in the search bar.
+     clearFocus() will actually send the focus to the first focusable thing in the layout.
+     If that focusable thing is still the search bar it will just reselect it, and the user won't
+     be able to deselect. So make a focusable view here to guarantee that we can clear the focus -->
+    <View
+        android:layout_width="1dp"
+        android:layout_height="1dp"
+        android:focusable="true"
+        android:focusableInTouchMode="true" />
+
+    <FrameLayout
+        android:id="@+id/content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/car_ui_toolbar_background"
+        style="@style/Widget.CarUi.Toolbar.Container"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/car_ui_toolbar_first_row_height"
+        android:tag="car_ui_top_inset"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/car_ui_toolbar_start_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_start_inset" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/car_ui_toolbar_top_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_top_inset" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/car_ui_toolbar_end_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_end="@dimen/car_ui_toolbar_end_inset" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/car_ui_toolbar_bottom_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            app:layout_constraintGuide_end="@dimen/car_ui_toolbar_bottom_inset" />
+
+        <!-- The horizontal bias set to 0.0 here is so that when you set this view as GONE, it will
+             be treated as if it's all the way to the left instead of centered in the margin -->
+        <FrameLayout
+            android:id="@+id/car_ui_toolbar_nav_icon_container"
+            style="@style/Widget.CarUi.Toolbar.NavIconContainer"
+            android:layout_width="@dimen/car_ui_toolbar_margin"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+            app:layout_constraintHorizontal_bias="0.0"
+            app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_start_guideline"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline">
+
+            <ImageView
+                android:id="@+id/car_ui_toolbar_nav_icon"
+                style="@style/Widget.CarUi.Toolbar.NavIcon"
+                android:layout_width="@dimen/car_ui_toolbar_nav_icon_size"
+                android:layout_height="@dimen/car_ui_toolbar_nav_icon_size"
+                android:layout_gravity="center"
+                android:scaleType="fitXY" />
+
+            <ImageView
+                android:id="@+id/car_ui_toolbar_logo"
+                android:layout_width="@dimen/car_ui_toolbar_logo_size"
+                android:layout_height="@dimen/car_ui_toolbar_logo_size"
+                android:layout_gravity="center"
+                android:scaleType="fitXY" />
+        </FrameLayout>
+
+        <FrameLayout
+            android:id="@+id/car_ui_toolbar_title_logo_container"
+            style="@style/Widget.CarUi.Toolbar.LogoContainer"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+            app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_nav_icon_container"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline">
+
+            <ImageView
+                android:id="@+id/car_ui_toolbar_title_logo"
+                style="@style/Widget.CarUi.Toolbar.Logo"
+                android:layout_width="@dimen/car_ui_toolbar_logo_size"
+                android:layout_height="@dimen/car_ui_toolbar_logo_size"
+                android:layout_gravity="center"
+                android:scaleType="fitXY" />
+        </FrameLayout>
+
+        <TextView
+            android:id="@+id/car_ui_toolbar_title"
+            style="@style/Widget.CarUi.Toolbar.Title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+            app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"
+            app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_title_logo_container"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline" />
+
+        <com.android.car.ui.toolbar.TabLayout
+            android:id="@+id/car_ui_toolbar_tabs"
+            android:layout_width="wrap_content"
+            android:layout_height="0dp"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+            app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"
+            app:layout_constraintHorizontal_bias="0.0"
+            app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_title_logo_container"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline" />
+
+        <LinearLayout
+            android:id="@+id/car_ui_toolbar_menu_items_container"
+            style="@style/Widget.CarUi.Toolbar.MenuItem.Container"
+            android:layout_width="wrap_content"
+            android:layout_height="0dp"
+            android:orientation="horizontal"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+            app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_end_guideline"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline" />
+
+        <FrameLayout
+            android:id="@+id/car_ui_toolbar_search_view_container"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/car_ui_toolbar_search_height"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+            app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"
+            app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_nav_icon_container"
+            app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline" />
+
+        <View
+            android:id="@+id/car_ui_toolbar_row_separator"
+            style="@style/Widget.CarUi.Toolbar.SeparatorView"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/car_ui_toolbar_separator_height"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+        <ProgressBar
+            android:id="@+id/car_ui_toolbar_progress_bar"
+            style="@style/Widget.CarUi.Toolbar.ProgressBar"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:indeterminate="true"
+            android:visibility="gone"
+            app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+        <View
+            android:id="@+id/car_ui_toolbar_bottom_styleable"
+            style="@style/Widget.CarUi.Toolbar.BottomView"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/car_ui_toolbar_bottom_view_height"
+            app:layout_constraintBottom_toTopOf="@+id/car_ui_toolbar_progress_bar"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
+
diff --git a/car-ui-lib/res/layout/car_ui_header_list_item.xml b/car-ui-lib/res/layout/car_ui_header_list_item.xml
index 2bb1fce..8f07636 100644
--- a/car-ui-lib/res/layout/car_ui_header_list_item.xml
+++ b/car-ui-lib/res/layout/car_ui_header_list_item.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="utf-8"?>
+<!--
   ~ Copyright 2019 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,8 +15,10 @@
   ~ limitations under the License.
   -->
 
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:background="@drawable/car_ui_list_header_background"
     android:layout_width="match_parent"
     android:layout_height="@dimen/car_ui_list_item_header_height">
 
diff --git a/car-ui-lib/res/layout/car_ui_list_item.xml b/car-ui-lib/res/layout/car_ui_list_item.xml
index 6be7e82..d838fcb 100644
--- a/car-ui-lib/res/layout/car_ui_list_item.xml
+++ b/car-ui-lib/res/layout/car_ui_list_item.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?><!--
+<?xml version="1.0" encoding="utf-8"?>
+<!--
   ~ Copyright 2019 The Android Open Source Project
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,10 +15,12 @@
   ~ limitations under the License.
   -->
 
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<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="@dimen/car_ui_list_item_height">
+    android:layout_height="wrap_content"
+    android:minHeight="@dimen/car_ui_list_item_height">
 
     <!-- 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. -->
@@ -25,7 +28,7 @@
         android:id="@+id/touch_interceptor"
         android:layout_width="0dp"
         android:layout_height="0dp"
-        android:background="?android:attr/selectableItemBackground"
+        android:background="@drawable/car_ui_list_item_background"
         android:clickable="true"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
@@ -37,7 +40,7 @@
         android:id="@+id/reduced_touch_interceptor"
         android:layout_width="0dp"
         android:layout_height="0dp"
-        android:background="?android:attr/selectableItemBackground"
+        android:background="@drawable/car_ui_list_item_background"
         android:clickable="true"
         android:visibility="gone"
         app:layout_constraintBottom_toBottomOf="parent"
@@ -65,6 +68,24 @@
             android:layout_width="@dimen/car_ui_list_item_icon_size"
             android:layout_height="@dimen/car_ui_list_item_icon_size"
             android:layout_gravity="center"
+            android:visibility="gone"
+            android:scaleType="fitXY" />
+
+        <ImageView
+            android:id="@+id/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"
+            android:visibility="gone"
+            android:scaleType="fitXY" />
+
+        <ImageView
+            android:id="@+id/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"
+            android:layout_gravity="center"
+            android:visibility="gone"
             android:scaleType="fitXY" />
     </FrameLayout>
 
@@ -94,11 +115,24 @@
         app:layout_constraintTop_toBottomOf="@+id/title"
         app:layout_goneMarginStart="@dimen/car_ui_list_item_text_no_icon_start_margin" />
 
+    <!-- This touch interceptor is sized and positioned to encompass the action container   -->
+    <View
+        android:id="@+id/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" />
+
     <FrameLayout
         android:id="@+id/action_container"
-        android:layout_width="@dimen/car_ui_list_item_icon_container_width"
+        android:layout_width="wrap_content"
+        android:minWidth="@dimen/car_ui_list_item_icon_container_width"
         android:layout_height="0dp"
-        android:background="?android:attr/selectableItemBackground"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="@+id/car_ui_list_item_end_guideline"
         app:layout_constraintTop_toTopOf="parent">
diff --git a/car-ui-lib/res/layout/car_ui_preference_fragment.xml b/car-ui-lib/res/layout/car_ui_preference_fragment.xml
index 1f3fb9a..7298055 100644
--- a/car-ui-lib/res/layout/car_ui_preference_fragment.xml
+++ b/car-ui-lib/res/layout/car_ui_preference_fragment.xml
@@ -32,10 +32,4 @@
             android:layout_height="match_parent"
             app:enableDivider="true"/>
     </FrameLayout>
-
-    <com.android.car.ui.toolbar.Toolbar
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:id="@+id/toolbar"
-        app:state="subpage"/>
 </FrameLayout>
diff --git a/car-ui-lib/res/layout/car_ui_preference_fragment_with_toolbar.xml b/car-ui-lib/res/layout/car_ui_preference_fragment_with_toolbar.xml
new file mode 100644
index 0000000..4acb10d
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_preference_fragment_with_toolbar.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright 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.
+-->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/car_ui_preference_fragment_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/car_ui_activity_background">
+
+    <FrameLayout
+        android:id="@android:id/list_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <com.android.car.ui.recyclerview.CarUiRecyclerView
+            android:id="@+id/recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:enableDivider="true"/>
+    </FrameLayout>
+
+    <com.android.car.ui.toolbar.Toolbar
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/toolbar"
+        app:state="subpage"/>
+</FrameLayout>
diff --git a/car-ui-lib/res/layout/car_ui_preference_widget_seekbar.xml b/car-ui-lib/res/layout/car_ui_preference_widget_seekbar.xml
index 9442e7e..e51059f 100644
--- a/car-ui-lib/res/layout/car_ui_preference_widget_seekbar.xml
+++ b/car-ui-lib/res/layout/car_ui_preference_widget_seekbar.xml
@@ -73,9 +73,7 @@
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:layout_weight="1"
-                android:background="@null"
-                android:clickable="false"
-                android:focusable="false"/>
+                style="@style/Widget.CarUi.SeekbarPreference.Seekbar"/>
 
             <TextView
                 android:id="@+id/seekbar_value"
diff --git a/car-ui-lib/res/layout/car_ui_recycler_view.xml b/car-ui-lib/res/layout/car_ui_recycler_view.xml
new file mode 100644
index 0000000..29150d7
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_recycler_view.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 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.
+  -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+  <include layout="@layout/car_ui_recyclerview_scrollbar"/>
+
+  <FrameLayout
+      android:id="@+id/car_ui_recycler_view"
+      android:layout_width="0dp"
+      android:layout_height="match_parent"
+      android:layout_marginEnd="@dimen/car_ui_scrollbar_margin"
+      android:layout_weight="1"/>
+</merge>
\ No newline at end of file
diff --git a/car-ui-lib/res/layout/car_ui_recyclerview_scrollbar.xml b/car-ui-lib/res/layout/car_ui_recyclerview_scrollbar.xml
index a5decce..ceadfaf 100644
--- a/car-ui-lib/res/layout/car_ui_recyclerview_scrollbar.xml
+++ b/car-ui-lib/res/layout/car_ui_recyclerview_scrollbar.xml
@@ -18,8 +18,9 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
-    android:layout_width="match_parent"
+    android:layout_width="@dimen/car_ui_scrollbar_container_width"
     android:layout_height="match_parent"
+    android:id="@+id/car_ui_scroll_bar"
     android:gravity="center">
 
     <ImageButton
diff --git a/car-ui-lib/res/layout/car_ui_toolbar.xml b/car-ui-lib/res/layout/car_ui_toolbar.xml
index 187dc84..f6ba021 100644
--- a/car-ui-lib/res/layout/car_ui_toolbar.xml
+++ b/car-ui-lib/res/layout/car_ui_toolbar.xml
@@ -19,6 +19,7 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="@dimen/car_ui_toolbar_first_row_height"
+    android:id="@+id/car_ui_toolbar_background"
     style="@style/Widget.CarUi.Toolbar.Container">
 
     <!-- When the user finishes searching, we call clearFocus() on the editText in the search bar.
diff --git a/car-ui-lib/res/layout/car_ui_toolbar_menu_item.xml b/car-ui-lib/res/layout/car_ui_toolbar_menu_item.xml
index 372f659..a24b1cf 100644
--- a/car-ui-lib/res/layout/car_ui_toolbar_menu_item.xml
+++ b/car-ui-lib/res/layout/car_ui_toolbar_menu_item.xml
@@ -17,10 +17,8 @@
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:minHeight="@dimen/car_ui_touch_target_height"
-    android:minWidth="@dimen/car_ui_touch_target_width"
-    android:layout_gravity="center_vertical">
+    android:layout_height="match_parent"
+    style="@style/Widget.CarUi.Toolbar.MenuItem.IndividualContainer">
     <FrameLayout
         android:id="@+id/car_ui_toolbar_menu_item_icon_container"
         android:layout_width="match_parent"
diff --git a/car-ui-lib/res/layout/car_ui_toolbar_tab_item.xml b/car-ui-lib/res/layout/car_ui_toolbar_tab_item.xml
index 82faedc..9172f5b 100644
--- a/car-ui-lib/res/layout/car_ui_toolbar_tab_item.xml
+++ b/car-ui-lib/res/layout/car_ui_toolbar_tab_item.xml
@@ -18,7 +18,7 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     style="@style/Widget.CarUi.Toolbar.Tab.Container">
     <ImageView
         android:id="@+id/car_ui_toolbar_tab_item_icon"
diff --git a/car-ui-lib/res/layout/car_ui_toolbar_tab_item_flexible.xml b/car-ui-lib/res/layout/car_ui_toolbar_tab_item_flexible.xml
new file mode 100644
index 0000000..44e4725
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_toolbar_tab_item_flexible.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="0dp"
+    android:layout_height="match_parent"
+    android:layout_weight="1"
+    style="@style/Widget.CarUi.Toolbar.Tab.Container">
+    <ImageView
+        android:id="@+id/car_ui_toolbar_tab_item_icon"
+        android:layout_width="@dimen/car_ui_toolbar_tab_icon_width"
+        android:layout_height="@dimen/car_ui_toolbar_tab_icon_height"
+        style="@style/Widget.CarUi.Toolbar.Tab.Icon"/>
+    <TextView
+        android:id="@+id/car_ui_toolbar_tab_item_text"
+        android:layout_width="@dimen/car_ui_toolbar_tab_text_width"
+        android:layout_height="wrap_content"
+        style="@style/Widget.CarUi.Toolbar.Tab.Text"/>
+</LinearLayout>
diff --git a/car-ui-lib/res/layout/car_ui_toolbar_two_row.xml b/car-ui-lib/res/layout/car_ui_toolbar_two_row.xml
index efae70b..428a2b6 100644
--- a/car-ui-lib/res/layout/car_ui_toolbar_two_row.xml
+++ b/car-ui-lib/res/layout/car_ui_toolbar_two_row.xml
@@ -19,6 +19,7 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:id="@+id/car_ui_toolbar_background"
     style="@style/Widget.CarUi.Toolbar.Container">
 
     <androidx.constraintlayout.widget.Guideline
diff --git a/car-ui-lib/res/values/attrs.xml b/car-ui-lib/res/values/attrs.xml
index 71ad091..8f7b3e0 100644
--- a/car-ui-lib/res/values/attrs.xml
+++ b/car-ui-lib/res/values/attrs.xml
@@ -14,6 +14,14 @@
  limitations under the License.
 -->
 <resources>
+    <!-- Global theme options for CarUi -->
+    <declare-styleable name="CarUi">
+        <!-- When set to true, the window decor will contain an OEM-customizable layout -->
+        <attr name="carUiBaseLayout" format="boolean"/>
+        <!-- When set to true, a CarUi Toolbar will be provided in the window decor -->
+        <attr name="carUiToolbar" format="boolean"/>
+    </declare-styleable>
+
     <declare-styleable name="CarUiToolbar">
         <!-- Title of the toolbar, only displayed in certain conditions -->
         <attr name="title" format="string"/>
@@ -39,6 +47,8 @@
         </attr>
         <!-- XML resource of MenuItems. See Toolbar.setMenuItems(int) for more information. -->
         <attr name="menuItems" format="reference"/>
+        <!-- Whether or not to show tabs in the SUBPAGE state. Default false -->
+        <attr name="showTabsInSubpage" format="boolean"/>
     </declare-styleable>
 
     <declare-styleable name="CarUiToolbarMenuItem">
diff --git a/car-ui-lib/res/values/bools.xml b/car-ui-lib/res/values/bools.xml
index d2c01c7..955956d 100644
--- a/car-ui-lib/res/values/bools.xml
+++ b/car-ui-lib/res/values/bools.xml
@@ -35,9 +35,6 @@
     <!-- Whether to display the Scroll Bar or not. Defaults to true. If this is set to false,
          the CarUiRecyclerView will behave exactly like the RecyclerView. -->
     <bool name="car_ui_scrollbar_enable">true</bool>
-    <!-- Whether to place the scrollbar z-index above the recycler view. Defaults to
-         true. -->
-    <bool name="car_ui_scrollbar_above_recycler_view">true</bool>
 
     <!-- Preferences -->
 
diff --git a/car-ui-lib/res/values/dimens.xml b/car-ui-lib/res/values/dimens.xml
index 171d3ba..3a77194 100644
--- a/car-ui-lib/res/values/dimens.xml
+++ b/car-ui-lib/res/values/dimens.xml
@@ -191,19 +191,27 @@
     <dimen name="car_ui_list_item_header_start_inset">0dp</dimen>
     <dimen name="car_ui_list_item_start_inset">0dp</dimen>
     <dimen name="car_ui_list_item_end_inset">0dp</dimen>
-    <dimen name="car_ui_list_item_text_start_margin">0dp</dimen>
+    <dimen name="car_ui_list_item_text_start_margin">24dp</dimen>
     <dimen name="car_ui_list_item_text_no_icon_start_margin">24dp</dimen>
+
+    <!-- List item icons  -->
+
     <dimen name="car_ui_list_item_icon_size">@dimen/car_ui_primary_icon_size</dimen>
+    <dimen name="car_ui_list_item_content_icon_width">@dimen/car_ui_list_item_icon_container_width</dimen>
+    <dimen name="car_ui_list_item_content_icon_height">@dimen/car_ui_list_item_icon_container_width</dimen>
+    <dimen name="car_ui_list_item_avatar_icon_width">@dimen/car_ui_primary_icon_size</dimen>
+    <dimen name="car_ui_list_item_avatar_icon_height">@dimen/car_ui_primary_icon_size</dimen>
     <dimen name="car_ui_list_item_supplemental_icon_size">@dimen/car_ui_primary_icon_size</dimen>
     <dimen name="car_ui_list_item_icon_container_width">112dp</dimen>
     <dimen name="car_ui_list_item_action_divider_width">1dp</dimen>
     <dimen name="car_ui_list_item_action_divider_height">60dp</dimen>
 
+    <!-- List item actions  -->
+
     <dimen name="car_ui_list_item_radio_button_height">@dimen/car_ui_list_item_height</dimen>
     <dimen name="car_ui_list_item_radio_button_start_inset">@dimen/car_ui_list_item_start_inset</dimen>
     <dimen name="car_ui_list_item_radio_button_end_inset">@dimen/car_ui_list_item_end_inset</dimen>
     <dimen name="car_ui_list_item_radio_button_icon_container_width">@dimen/car_ui_list_item_icon_container_width</dimen>
-
     <dimen name="car_ui_list_item_check_box_height">@dimen/car_ui_list_item_height</dimen>
     <dimen name="car_ui_list_item_check_box_start_inset">@dimen/car_ui_list_item_start_inset</dimen>
     <dimen name="car_ui_list_item_check_box_end_inset">@dimen/car_ui_list_item_end_inset</dimen>
diff --git a/car-ui-lib/res/values/drawables.xml b/car-ui-lib/res/values/drawables.xml
index c5cbc60..181846c 100644
--- a/car-ui-lib/res/values/drawables.xml
+++ b/car-ui-lib/res/values/drawables.xml
@@ -27,9 +27,13 @@
     <drawable name="car_ui_toolbar_search_search_icon">@drawable/car_ui_icon_search</drawable>
     <!-- Icon used for clearing the search box in toolbar -->
     <drawable name="car_ui_toolbar_search_close_icon">@drawable/car_ui_icon_close</drawable>
+    <!-- Icon used for nav when the toolbar is in search state   -->
+    <drawable name="car_ui_icon_search_nav_icon">@drawable/car_ui_icon_arrow_back</drawable>
 
     <!-- Preferences -->
 
-    <!-- Overlayable drawable to use for the preference chevron -->
-    <item name="car_ui_preference_icon_chevron" type="drawable">@null</item>
+    <!-- Overlayable drawable to use for the preference chevron when preference is enabled -->
+    <item name="car_ui_preference_icon_chevron_enabled" type="drawable">@null</item>
+    <!-- Overlayable drawable to use for the preference chevron when preference is disabled -->
+    <item name="car_ui_preference_icon_chevron_disabled" type="drawable">@null</item>
 </resources>
diff --git a/car-ui-lib/res/values/integers.xml b/car-ui-lib/res/values/integers.xml
index fd017b7..623ef00 100644
--- a/car-ui-lib/res/values/integers.xml
+++ b/car-ui-lib/res/values/integers.xml
@@ -17,20 +17,4 @@
 <resources>
     <!-- Default max string length -->
     <integer name="car_ui_default_max_string_length">120</integer>
-    <!--
-    Whether to include a gutter to the start, end or both sides of the list view items.
-    The gutter width will be the width of the scrollbar, and by default will be set to
-    both. Values are defined as follows:
-      none = 0
-      start = 1
-      end = 2
-      both = 3
-    -->
-    <integer name="car_ui_scrollbar_gutter">3</integer>
-    <!--
-    Position of the scrollbar. Default to left. Values are defined as follows:
-      start = 0
-      end = 1
-    -->
-    <integer name="car_ui_scrollbar_position">0</integer>
 </resources>
\ No newline at end of file
diff --git a/car-ui-lib/res/values/styles.xml b/car-ui-lib/res/values/styles.xml
index d22753f..9cbf41e 100644
--- a/car-ui-lib/res/values/styles.xml
+++ b/car-ui-lib/res/values/styles.xml
@@ -26,6 +26,8 @@
 
     <style name="Widget.CarUi.Toolbar"/>
 
+    <style name="Widget.CarUi.SeekbarPreference"/>
+
     <style name="Widget.CarUi.Toolbar.Container"/>
 
     <style name="Widget.CarUi.Toolbar.NavIconContainer"/>
@@ -49,6 +51,7 @@
     <style name="Widget.CarUi.Toolbar.Title">
         <item name="android:layout_marginStart">@dimen/car_ui_toolbar_title_margin_start</item>
         <item name="android:textAppearance">@style/TextAppearance.CarUi.Widget.Toolbar.Title</item>
+        <item name="android:textDirection">locale</item>
     </style>
 
     <style name="Widget.CarUi.Toolbar.TextButton" parent="Widget.CarUi.Button.Borderless.Colored">
@@ -60,6 +63,13 @@
         <item name="android:textColor">@color/car_ui_toolbar_menu_item_icon_color</item>
     </style>
 
+    <!-- Style applied to the seekbar widget within the seekbar preference -->
+    <style name="Widget.CarUi.SeekbarPreference.Seekbar">
+        <item name="android:background">@null</item>
+        <item name="android:clickable">false</item>
+        <item name="android:focusable">false</item>
+    </style>
+
     <!-- Style applied to the decoration view between toolbar rows -->
     <style name="Widget.CarUi.Toolbar.SeparatorView">
         <item name="android:height">0.01dp</item>
@@ -72,11 +82,19 @@
         <item name="android:background">@android:color/transparent</item>
     </style>
 
-    <style name="Widget.CarUi.Toolbar.MenuItem.Container" parent="@style/Widget.CarUi.Toolbar">
+    <style name="Widget.CarUi.Toolbar.MenuItem"/>
+
+    <style name="Widget.CarUi.Toolbar.MenuItem.Container">
         <item name="android:divider">@drawable/car_ui_toolbar_menu_item_divider</item>
         <item name="android:showDividers">beginning|middle|end</item>
     </style>
 
+    <style name="Widget.CarUi.Toolbar.MenuItem.IndividualContainer">
+        <item name="android:minHeight">@dimen/car_ui_touch_target_height</item>
+        <item name="android:minWidth">@dimen/car_ui_touch_target_width</item>
+        <item name="android:layout_gravity">center_vertical</item>
+    </style>
+
     <!-- Style applied to the edit box inside the toolbar search area -->
     <style name="Widget.CarUi.Toolbar.Search.EditText"
         parent="android:Widget.DeviceDefault.EditText"/>
@@ -113,10 +131,6 @@
         <item name="android:scrollbars">vertical</item>
     </style>
 
-    <style name="Widget.CarUi.CarUiRecyclerView.NestedRecyclerView">
-        <item name="android:scrollbars">none</item>
-    </style>
-
     <style name="Widget.CarUi.AlertDialog"/>
 
     <style name="Widget.CarUi.AlertDialog.HeaderContainer">
@@ -140,6 +154,8 @@
     <!-- Preference Styles -->
 
     <style name="Preference.CarUi">
+        <item name="allowDividerBelow">false</item>
+        <item name="allowDividerAbove">false</item>
         <item name="android:layout">@layout/car_ui_preference</item>
     </style>
 
@@ -192,6 +208,12 @@
 
     <style name="PreferenceFragment.CarUi">
         <item name="android:divider">?android:attr/listDivider</item>
+        <!-- TODO(b/150230923) change this to car_ui_preference_fragment -->
+        <item name="android:layout">@layout/car_ui_preference_fragment_with_toolbar</item>
+    </style>
+
+    <!-- TODO(b/150230923) remove this when other apps are ready -->
+    <style name="PreferenceFragment.CarUi.WithToolbar">
         <item name="android:layout">@layout/car_ui_preference_fragment</item>
     </style>
 
@@ -210,6 +232,18 @@
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
+    <style name="TextAppearance.CarUi.Body1">
+        <item name="android:textSize">@dimen/car_ui_body1_size</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.Body2">
+        <item name="android:textSize">@dimen/car_ui_body2_size</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.Body3">
+        <item name="android:textSize">@dimen/car_ui_body3_size</item>
+    </style>
+
     <style name="TextAppearance.CarUi.PreferenceCategoryTitle">
         <item name="android:fontFamily">@string/car_ui_preference_category_title_font_family</item>
         <item name="android:textColor">@color/car_ui_preference_category_title_text_color</item>
diff --git a/car-ui-lib/res/values/themes.xml b/car-ui-lib/res/values/themes.xml
index 91d26c6..34bbf3c 100644
--- a/car-ui-lib/res/values/themes.xml
+++ b/car-ui-lib/res/values/themes.xml
@@ -16,6 +16,10 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
     <!-- TODO: for internal TODOs, expand theme/style to leaf resources as necessary -->
     <style name="Theme.CarUi" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+        <!-- TODO(b/150230923) change to true when other apps are ready -->
+        <item name="carUiBaseLayout">false</item>
+        <item name="carUiToolbar">false</item>
+
         <!-- Attributes from: Base.V7.Theme.AppCompat -->
 
         <item name="windowNoTitle">true</item>
@@ -202,6 +206,18 @@
         <item name="carUiRecyclerViewStyle">@style/Widget.CarUi.CarUiRecyclerView</item>
     </style>
 
+    <!-- TODO(b/150230923) remove this when other apps are ready -->
+    <style name="Theme.CarUi.WithToolbar">
+        <item name="carUiBaseLayout">true</item>
+        <item name="carUiToolbar">true</item>
+        <item name="preferenceTheme">@style/CarUiPreferenceTheme.WithToolbar</item>
+    </style>
+
+    <style name="Theme.CarUi.NoToolbar">
+        <item name="carUiBaseLayout">true</item>
+        <item name="carUiToolbar">false</item>
+    </style>
+
     <style name="CarUiPreferenceTheme">
         <item name="checkBoxPreferenceStyle">@style/Preference.CarUi.CheckBoxPreference</item>
         <item name="dialogPreferenceStyle">@style/Preference.CarUi.DialogPreference</item>
@@ -217,4 +233,10 @@
         <item name="switchPreferenceStyle">@style/Preference.CarUi.SwitchPreference</item>
     </style>
 
+    <!-- TODO(b/150230923) remove this when other apps are ready -->
+    <style name="CarUiPreferenceTheme.WithToolbar">
+        <item name="preferenceFragmentCompatStyle">@style/PreferenceFragment.CarUi.WithToolbar</item>
+        <item name="preferenceFragmentStyle">@style/PreferenceFragment.CarUi.WithToolbar</item>
+    </style>
+
 </resources>
diff --git a/car-ui-lib/res/values/values.xml b/car-ui-lib/res/values/values.xml
index 2ad44fa..82a4d65 100644
--- a/car-ui-lib/res/values/values.xml
+++ b/car-ui-lib/res/values/values.xml
@@ -20,4 +20,5 @@
 
     <!-- Layout to be used for toolbar tabs -->
     <item name="car_ui_toolbar_tab_item_layout" type="layout">@layout/car_ui_toolbar_tab_item</item>
+    <item name="car_ui_toolbar_tab_item_layout_flexible" type="layout">@layout/car_ui_toolbar_tab_item_flexible</item>
 </resources>
diff --git a/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java b/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java
index 76b9dc6..4803b45 100644
--- a/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java
+++ b/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java
@@ -34,7 +34,13 @@
 import androidx.annotation.ArrayRes;
 import androidx.annotation.AttrRes;
 import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
 import androidx.annotation.StringRes;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.recyclerview.CarUiListItemAdapter;
+import com.android.car.ui.recyclerview.CarUiRadioButtonListItemAdapter;
 
 /**
  * Wrapper for AlertDialog.Builder
@@ -324,14 +330,11 @@
     }
 
     /**
-     * Set a list of items, which are supplied by the given {@link ListAdapter}, to be
-     * displayed in the dialog as the content, you will be notified of the
-     * selected item via the supplied listener.
+     * This was not supposed to be in the Chassis API because it allows custom views.
      *
-     * @param adapter The {@link ListAdapter} to supply the list of items
-     * @param listener The listener that will be called when an item is clicked.
-     * @return This Builder object to allow for chaining of calls to set methods
+     * @deprecated Use {@link #setAdapter(CarUiListItemAdapter)} instead.
      */
+    @Deprecated
     public AlertDialogBuilder setAdapter(final ListAdapter adapter,
             final DialogInterface.OnClickListener listener) {
         mBuilder.setAdapter(adapter, listener);
@@ -339,6 +342,25 @@
     }
 
     /**
+     * Display all the {@link com.android.car.ui.recyclerview.CarUiListItem CarUiListItems} in a
+     * {@link CarUiListItemAdapter}. You should set click listeners on the CarUiListItems as
+     * opposed to a callback in this function.
+     */
+    public AlertDialogBuilder setAdapter(final CarUiListItemAdapter adapter) {
+        setCustomList(adapter);
+        return this;
+    }
+
+    private void setCustomList(@NonNull CarUiListItemAdapter adapter) {
+        View customList = LayoutInflater.from(mContext).inflate(
+                R.layout.car_ui_alert_dialog_list, null);
+        RecyclerView mList = customList.requireViewById(R.id.list);
+        mList.setLayoutManager(new LinearLayoutManager(mContext));
+        mList.setAdapter(adapter);
+        mBuilder.setView(customList);
+    }
+
+    /**
      * Set a list of items, which are supplied by the given {@link Cursor}, to be
      * displayed in the dialog as the content, you will be notified of the
      * selected item via the supplied listener.
@@ -488,21 +510,51 @@
     }
 
     /**
+     * This was not supposed to be in the Chassis API because it allows custom views.
+     *
+     * @deprecated Use {@link #setSingleChoiceItems(CarUiRadioButtonListItemAdapter,
+     * DialogInterface.OnClickListener)} instead.
+     */
+    @Deprecated
+    public AlertDialogBuilder setSingleChoiceItems(ListAdapter adapter, int checkedItem,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setSingleChoiceItems(adapter, checkedItem, listener);
+        return this;
+    }
+
+    /**
      * Set a list of items to be displayed in the dialog as the content, you will be notified of
      * the selected item via the supplied listener. The list will have a check mark displayed to
      * the right of the text for the checked item. Clicking on an item in the list will not
      * dismiss the dialog. Clicking on a button will dismiss the dialog.
      *
-     * @param adapter The {@link ListAdapter} to supply the list of items
-     * @param checkedItem specifies which item is checked. If -1 no items are checked.
+     * @param adapter The {@link CarUiRadioButtonListItemAdapter} to supply the list of items
      * @param listener notified when an item on the list is clicked. The dialog will not be
      * dismissed when an item is clicked. It will only be dismissed if clicked on a
      * button, if no buttons are supplied it's up to the user to dismiss the dialog.
      * @return This Builder object to allow for chaining of calls to set methods
+     *
+     * @deprecated Use {@link #setSingleChoiceItems(CarUiRadioButtonListItemAdapter)} instead.
      */
-    public AlertDialogBuilder setSingleChoiceItems(ListAdapter adapter, int checkedItem,
+    @Deprecated
+    public AlertDialogBuilder setSingleChoiceItems(CarUiRadioButtonListItemAdapter adapter,
             final DialogInterface.OnClickListener listener) {
-        mBuilder.setSingleChoiceItems(adapter, checkedItem, listener);
+        setCustomList(adapter);
+        return this;
+    }
+
+    /**
+     * Set a list of items to be displayed in the dialog as the content,The list will have a check
+     * mark displayed to the right of the text for the checked item. Clicking on an item in the list
+     * will not dismiss the dialog. Clicking on a button will dismiss the dialog.
+     *
+     * @param adapter The {@link CarUiRadioButtonListItemAdapter} to supply the list of items
+     * dismissed when an item is clicked. It will only be dismissed if clicked on a
+     * button, if no buttons are supplied it's up to the user to dismiss the dialog.
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setSingleChoiceItems(CarUiRadioButtonListItemAdapter adapter) {
+        setCustomList(adapter);
         return this;
     }
 
diff --git a/car-ui-lib/src/com/android/car/ui/baselayout/Insets.java b/car-ui-lib/src/com/android/car/ui/baselayout/Insets.java
new file mode 100644
index 0000000..e45a4b6
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/baselayout/Insets.java
@@ -0,0 +1,81 @@
+/*
+ * 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.baselayout;
+
+import java.util.Objects;
+
+/**
+ * A representation of the insets into the content view that the user-accessible
+ * content should have.
+ *
+ * See {@link InsetsChangedListener} for more information.
+ */
+public final class Insets {
+    private final int mLeft;
+    private final int mRight;
+    private final int mTop;
+    private final int mBottom;
+
+    public Insets() {
+        mLeft = mRight = mTop = mBottom = 0;
+    }
+
+    public Insets(int left, int top, int right, int bottom) {
+        mLeft = left;
+        mRight = right;
+        mTop = top;
+        mBottom = bottom;
+    }
+
+    public int getLeft() {
+        return mLeft;
+    }
+
+    public int getRight() {
+        return mRight;
+    }
+
+    public int getTop() {
+        return mTop;
+    }
+
+    public int getBottom() {
+        return mBottom;
+    }
+
+    @Override
+    public String toString() {
+        return "{ left: " + mLeft + ", right: " + mRight
+                + ", top: " + mTop + ", bottom: " + mBottom + " }";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Insets insets = (Insets) o;
+        return mLeft == insets.mLeft
+                && mRight == insets.mRight
+                && mTop == insets.mTop
+                && mBottom == insets.mBottom;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mLeft, mRight, mTop, mBottom);
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/baselayout/InsetsChangedListener.java b/car-ui-lib/src/com/android/car/ui/baselayout/InsetsChangedListener.java
new file mode 100644
index 0000000..595ad06
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/baselayout/InsetsChangedListener.java
@@ -0,0 +1,31 @@
+/*
+ * 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.baselayout;
+
+/**
+ * Interface for receiving changes to {@link Insets}.
+ *
+ * <p>This interface can be applied to either activities or fragments. CarUi will automatically call
+ * it when the insets change.
+ *
+ * <p>When neither the activity nor any of its fragments implement this interface, the Insets
+ * will be applied as padding to the content view.
+ */
+public interface InsetsChangedListener {
+    /** Called when the insets change */
+    void onCarUiInsetsChanged(Insets insets);
+}
diff --git a/car-ui-lib/src/com/android/car/ui/core/BaseLayoutController.java b/car-ui-lib/src/com/android/car/ui/core/BaseLayoutController.java
new file mode 100644
index 0000000..f488e9c
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/core/BaseLayoutController.java
@@ -0,0 +1,328 @@
+/*
+ * 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.utils.CarUiUtils.requireViewByRefId;
+
+import android.app.Activity;
+import android.content.res.TypedArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.car.ui.R;
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.toolbar.ToolbarController;
+import com.android.car.ui.toolbar.ToolbarControllerImpl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * BaseLayoutController accepts an {@link Activity} and sets up the base layout inside of it.
+ * It also exposes a {@link ToolbarController} to access the toolbar. This may be null if
+ * used with a base layout without a Toolbar.
+ */
+class BaseLayoutController {
+
+    private static Map<Activity, BaseLayoutController> sBaseLayoutMap = new HashMap<>();
+
+    private InsetsUpdater mInsetsUpdater;
+
+    /**
+     * Gets a BaseLayoutController for the given {@link Activity}. Must have called
+     * {@link #build(Activity)} with the same activity earlier, otherwise will return null.
+     */
+    @Nullable
+    /* package */ static BaseLayoutController getBaseLayout(Activity activity) {
+        return sBaseLayoutMap.get(activity);
+    }
+
+    @Nullable
+    private ToolbarController mToolbarController;
+
+    private BaseLayoutController(Activity activity) {
+        installBaseLayout(activity);
+    }
+
+    /**
+     * Create a new BaseLayoutController for the given {@link Activity}.
+     *
+     * <p>You can get a reference to it by calling {@link #getBaseLayout(Activity)}.
+     */
+    /* package */ static void build(Activity activity) {
+        sBaseLayoutMap.put(activity, new BaseLayoutController(activity));
+    }
+
+    /**
+     * Destroy the BaseLayoutController for the given {@link Activity}.
+     */
+    /* package */ static void destroy(Activity activity) {
+        sBaseLayoutMap.remove(activity);
+    }
+
+    /**
+     * Gets the {@link ToolbarController} for activities created with carUiBaseLayout and
+     * carUiToolbar set to true.
+     */
+    @Nullable
+    /* package */ ToolbarController getToolbarController() {
+        return mToolbarController;
+    }
+
+    /* package */ Insets getInsets() {
+        return mInsetsUpdater.getInsets();
+    }
+
+    /**
+     * Installs the base layout into an activity, moving its content view under the base layout.
+     *
+     * <p>This function must be called during the onCreate() of the {@link Activity}.
+     *
+     * @param activity The {@link Activity} to install a base layout in.
+     */
+    private void installBaseLayout(Activity activity) {
+        boolean baseLayoutEnabled = getThemeBoolean(activity, R.attr.carUiBaseLayout);
+        boolean toolbarEnabled = getThemeBoolean(activity, R.attr.carUiToolbar);
+        if (!baseLayoutEnabled) {
+            return;
+        }
+
+        @LayoutRes final int baseLayoutRes = toolbarEnabled
+                ? R.layout.car_ui_base_layout_toolbar
+                : R.layout.car_ui_base_layout;
+
+        View baseLayout = LayoutInflater.from(activity)
+                .inflate(baseLayoutRes, null, false);
+
+        // Replace windowContentView with baseLayout
+        ViewGroup windowContentView = activity.getWindow().findViewById(android.R.id.content);
+        ViewGroup contentViewParent = (ViewGroup) windowContentView.getParent();
+        int contentIndex = contentViewParent.indexOfChild(windowContentView);
+        contentViewParent.removeView(windowContentView);
+        contentViewParent.addView(baseLayout, contentIndex, windowContentView.getLayoutParams());
+
+        // Add windowContentView to the baseLayout's content view
+        FrameLayout contentView = requireViewByRefId(baseLayout, R.id.content);
+        contentView.addView(windowContentView, new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT));
+
+        if (toolbarEnabled) {
+            mToolbarController = new ToolbarControllerImpl(baseLayout);
+        }
+
+        mInsetsUpdater = new InsetsUpdater(activity, baseLayout, windowContentView);
+        mInsetsUpdater.installListeners();
+    }
+
+    /**
+     * Gets the boolean value of an Attribute from an {@link Activity Activity's}
+     * {@link android.content.res.Resources.Theme}.
+     */
+    private boolean getThemeBoolean(Activity activity, int attr) {
+        TypedArray a = activity.getTheme().obtainStyledAttributes(new int[] { attr });
+
+        try {
+            return a.getBoolean(0, false);
+        } finally {
+            a.recycle();
+        }
+    }
+
+    /**
+     * InsetsUpdater waits for layout changes, and when there is one, calculates the appropriate
+     * insets into the content view.
+     *
+     * <p>It then calls {@link InsetsChangedListener#onCarUiInsetsChanged(Insets)} on the
+     * {@link Activity} and any {@link Fragment Fragments} the Activity might have. If
+     * none of the Activity/Fragments implement {@link InsetsChangedListener}, it will set
+     * padding on the content view equal to the insets.
+     */
+    private static class InsetsUpdater implements ViewTreeObserver.OnGlobalLayoutListener {
+        // These tags mark views that should overlay the content view in the base layout.
+        // OEMs should add them to views in their base layout, ie: android:tag="car_ui_left_inset"
+        // Apps will then be able to draw under these views, but will be encouraged to not put
+        // any user-interactable content there.
+        private static final String LEFT_INSET_TAG = "car_ui_left_inset";
+        private static final String RIGHT_INSET_TAG = "car_ui_right_inset";
+        private static final String TOP_INSET_TAG = "car_ui_top_inset";
+        private static final String BOTTOM_INSET_TAG = "car_ui_bottom_inset";
+
+        private final Activity mActivity;
+        private final View mLeftInsetView;
+        private final View mRightInsetView;
+        private final View mTopInsetView;
+        private final View mBottomInsetView;
+
+        private boolean mInsetsDirty = true;
+        @NonNull
+        private Insets mInsets = new Insets();
+
+        /**
+         * Constructs an InsetsUpdater that calculates and dispatches insets to an {@link Activity}.
+         *
+         * @param activity The activity that is using base layouts
+         * @param baseLayout The root view of the base layout
+         * @param contentView The android.R.id.content View
+         */
+        InsetsUpdater(Activity activity, View baseLayout, View contentView) {
+            mActivity = activity;
+
+            mLeftInsetView = baseLayout.findViewWithTag(LEFT_INSET_TAG);
+            mRightInsetView = baseLayout.findViewWithTag(RIGHT_INSET_TAG);
+            mTopInsetView = baseLayout.findViewWithTag(TOP_INSET_TAG);
+            mBottomInsetView = baseLayout.findViewWithTag(BOTTOM_INSET_TAG);
+
+            final View.OnLayoutChangeListener layoutChangeListener =
+                    (View v, int left, int top, int right, int bottom,
+                            int oldLeft, int oldTop, int oldRight, int oldBottom) -> {
+                        if (left != oldLeft || top != oldTop
+                                || right != oldRight || bottom != oldBottom) {
+                            mInsetsDirty = true;
+                        }
+                    };
+
+            if (mLeftInsetView != null) {
+                mLeftInsetView.addOnLayoutChangeListener(layoutChangeListener);
+            }
+            if (mRightInsetView != null) {
+                mRightInsetView.addOnLayoutChangeListener(layoutChangeListener);
+            }
+            if (mTopInsetView != null) {
+                mTopInsetView.addOnLayoutChangeListener(layoutChangeListener);
+            }
+            if (mBottomInsetView != null) {
+                mBottomInsetView.addOnLayoutChangeListener(layoutChangeListener);
+            }
+            contentView.addOnLayoutChangeListener(layoutChangeListener);
+        }
+
+        /**
+         * Install a global layout listener, during which the insets will be recalculated and
+         * dispatched.
+         */
+        void installListeners() {
+            // The global layout listener will run after all the individual layout change listeners
+            // so that we only updateInsets once per layout, even if multiple inset views changed
+            mActivity.getWindow().getDecorView().getViewTreeObserver()
+                    .addOnGlobalLayoutListener(this);
+        }
+
+        @NonNull
+        Insets getInsets() {
+            return mInsets;
+        }
+
+        /**
+         * onGlobalLayout() should recalculate the amount of insets we need, and then dispatch them.
+         */
+        @Override
+        public void onGlobalLayout() {
+            if (!mInsetsDirty) {
+                return;
+            }
+
+            View content = mActivity.requireViewById(android.R.id.content);
+
+            // Calculate how much each inset view overlays the content view
+            int top, bottom, left, right;
+            top = bottom = left = right = 0;
+            if (mTopInsetView != null) {
+                top = Math.max(0, getBottomOfView(mTopInsetView) - getTopOfView(content));
+            }
+            if (mBottomInsetView != null) {
+                bottom = Math.max(0, getBottomOfView(content) - getTopOfView(mBottomInsetView));
+            }
+            if (mLeftInsetView != null) {
+                left = Math.max(0, getRightOfView(mLeftInsetView) - getLeftOfView(content));
+            }
+            if (mRightInsetView != null) {
+                right = Math.max(0, getRightOfView(content) - getLeftOfView(mRightInsetView));
+            }
+            Insets insets = new Insets(left, top, right, bottom);
+
+            mInsetsDirty = false;
+            if (!insets.equals(mInsets)) {
+                mInsets = insets;
+                dispatchNewInsets(insets);
+            }
+        }
+
+        /**
+         * Dispatch the new {@link Insets} to the {@link Activity} and all of its
+         * {@link Fragment Fragments}. If none of those implement {@link InsetsChangedListener},
+         * we will set the value of the insets as padding on the content view.
+         *
+         * @param insets The newly-changed insets.
+         */
+        private void dispatchNewInsets(Insets insets) {
+            boolean handled = false;
+            if (mActivity instanceof InsetsChangedListener) {
+                ((InsetsChangedListener) mActivity).onCarUiInsetsChanged(insets);
+                handled = true;
+            }
+
+            if (mActivity instanceof FragmentActivity) {
+                for (Fragment fragment : ((FragmentActivity) mActivity).getSupportFragmentManager()
+                        .getFragments()) {
+                    if (fragment instanceof InsetsChangedListener) {
+                        ((InsetsChangedListener) fragment).onCarUiInsetsChanged(insets);
+                        handled = true;
+                    }
+                }
+            }
+
+            if (!handled) {
+                mActivity.requireViewById(android.R.id.content).setPadding(
+                        insets.getLeft(), insets.getTop(), insets.getRight(), insets.getBottom());
+            }
+        }
+
+        private static int getLeftOfView(View v) {
+            int[] position = new int[2];
+            v.getLocationOnScreen(position);
+            return position[0];
+        }
+
+        private static int getRightOfView(View v) {
+            int[] position = new int[2];
+            v.getLocationOnScreen(position);
+            return position[0] + v.getWidth();
+        }
+
+        private static int getTopOfView(View v) {
+            int[] position = new int[2];
+            v.getLocationOnScreen(position);
+            return position[1];
+        }
+
+        private static int getBottomOfView(View v) {
+            int[] position = new int[2];
+            v.getLocationOnScreen(position);
+            return position[1] + v.getHeight();
+        }
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/core/CarUi.java b/car-ui-lib/src/com/android/car/ui/core/CarUi.java
new file mode 100644
index 0000000..21050be
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/core/CarUi.java
@@ -0,0 +1,102 @@
+/*
+ * 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.app.Activity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.toolbar.ToolbarController;
+
+/**
+ * Public interface for general CarUi static functions.
+ */
+public class CarUi {
+
+    /**
+     * Gets the {@link ToolbarController} for an activity. Requires that the Activity uses
+     * Theme.CarUi.WithToolbar, or otherwise sets carUiBaseLayout and carUiToolbar to true.
+     *
+     * See also: {@link #requireToolbar(Activity)}
+     */
+    @Nullable
+    public static ToolbarController getToolbar(Activity activity) {
+        BaseLayoutController controller = BaseLayoutController.getBaseLayout(activity);
+        if (controller != null) {
+            return controller.getToolbarController();
+        }
+        return null;
+    }
+
+    /**
+     * Gets the {@link ToolbarController} for an activity. Requires that the Activity uses
+     * Theme.CarUi.WithToolbar, or otherwise sets carUiBaseLayout and carUiToolbar to true.
+     *
+     * <p>See also: {@link #getToolbar(Activity)}
+     *
+     * @throws IllegalArgumentException When the CarUi Toolbar cannot be found.
+     */
+    @NonNull
+    public static ToolbarController requireToolbar(Activity activity) {
+        ToolbarController result = getToolbar(activity);
+        if (result == null) {
+            throw new IllegalArgumentException("Activity does not have a CarUi Toolbar! "
+                    + "Are you using Theme.CarUi.WithToolbar?");
+        }
+
+        return result;
+    }
+
+    /**
+     * Gets the current {@link Insets} of the given {@link Activity}. Only applies to Activities
+     * using the base layout, ie have the theme attribute "carUiBaseLayout" set to true.
+     *
+     * <p>Note that you likely don't want to use this without also using
+     * {@link com.android.car.ui.baselayout.InsetsChangedListener}, as without it the Insets
+     * will automatically be applied to your Activity's content view.
+     */
+    @Nullable
+    public static Insets getInsets(Activity activity) {
+        BaseLayoutController controller = BaseLayoutController.getBaseLayout(activity);
+        if (controller != null) {
+            return controller.getInsets();
+        }
+        return null;
+    }
+
+    /**
+     * Gets the current {@link Insets} of the given {@link Activity}. Only applies to Activities
+     * using the base layout, ie have the theme attribute "carUiBaseLayout" set to true.
+     *
+     * <p>Note that you likely don't want to use this without also using
+     * {@link com.android.car.ui.baselayout.InsetsChangedListener}, as without it the Insets
+     * will automatically be applied to your Activity's content view.
+     *
+     * @throws IllegalArgumentException When the activity is not using base layouts.
+     */
+    @NonNull
+    public static Insets requireInsets(Activity activity) {
+        Insets result = getInsets(activity);
+        if (result == null) {
+            throw new IllegalArgumentException("Activity does not have a base layout! "
+                    + "Are you using Theme.CarUi.WithToolbar or Theme.CarUi.NoToolbar?");
+        }
+
+        return result;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/core/CarUiInstaller.java b/car-ui-lib/src/com/android/car/ui/core/CarUiInstaller.java
new file mode 100644
index 0000000..1aaa375
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/core/CarUiInstaller.java
@@ -0,0 +1,105 @@
+/*
+ * 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.app.Activity;
+import android.app.Application;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * {@link ContentProvider ContentProvider's} onCreate() methods are "called for all registered
+ * content providers on the application main thread at application launch time." This means we
+ * can use a content provider to register for Activity lifecycle callbacks before any activities
+ * have started, for installing the CarUi base layout into all activities.
+ */
+public class CarUiInstaller extends ContentProvider {
+
+    @Override
+    public boolean onCreate() {
+        Application application = (Application) getContext().getApplicationContext();
+        application.registerActivityLifecycleCallbacks(
+                new Application.ActivityLifecycleCallbacks() {
+                    @Override
+                    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+                        BaseLayoutController.build(activity);
+                    }
+
+                    @Override
+                    public void onActivityStarted(Activity activity) {
+                    }
+
+                    @Override
+                    public void onActivityResumed(Activity activity) {
+                    }
+
+                    @Override
+                    public void onActivityPaused(Activity activity) {
+                    }
+
+                    @Override
+                    public void onActivityStopped(Activity activity) {
+                    }
+
+                    @Override
+                    public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+                    }
+
+                    @Override
+                    public void onActivityDestroyed(Activity activity) {
+                        BaseLayoutController.destroy(activity);
+                    }
+                });
+        return true;
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
+            @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public String getType(@NonNull Uri uri) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public int delete(@NonNull Uri uri, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        return 0;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceFragment.java b/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceFragment.java
index a213a11..2543fb5 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceFragment.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceFragment.java
@@ -109,8 +109,8 @@
 
         for (int i = 0; i < entries.length; i++) {
             String entry = entries[i].toString();
-            CarUiContentListItem item = new CarUiContentListItem();
-            item.setAction(CarUiContentListItem.Action.RADIO_BUTTON);
+            CarUiContentListItem item = new CarUiContentListItem(
+                    CarUiContentListItem.Action.RADIO_BUTTON);
             item.setTitle(entry);
 
             if (i == selectedEntryIndex) {
@@ -118,7 +118,7 @@
                 mSelectedItem = item;
             }
 
-            item.setOnCheckedChangedListener((listItem, isChecked) -> {
+            item.setOnCheckedChangeListener((listItem, isChecked) -> {
                 if (mSelectedItem != null) {
                     mSelectedItem.setChecked(false);
                     adapter.notifyItemChanged(listItems.indexOf(mSelectedItem));
diff --git a/car-ui-lib/src/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java b/car-ui-lib/src/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
index e505708..c445142 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
@@ -114,11 +114,11 @@
         for (int i = 0; i < entries.length; i++) {
             String entry = entries[i].toString();
             String entryValue = entryValues[i].toString();
-            CarUiContentListItem item = new CarUiContentListItem();
-            item.setAction(CarUiContentListItem.Action.CHECK_BOX);
+            CarUiContentListItem item = new CarUiContentListItem(
+                    CarUiContentListItem.Action.CHECK_BOX);
             item.setTitle(entry);
             item.setChecked(selectedItems[i]);
-            item.setOnCheckedChangedListener((listItem, isChecked) -> {
+            item.setOnCheckedChangeListener((listItem, isChecked) -> {
                 if (isChecked) {
                     mNewValues.add(entryValue);
                 } else {
diff --git a/car-ui-lib/src/com/android/car/ui/preference/PreferenceFragment.java b/car-ui-lib/src/com/android/car/ui/preference/PreferenceFragment.java
index de25cf8..bff6c13 100644
--- a/car-ui-lib/src/com/android/car/ui/preference/PreferenceFragment.java
+++ b/car-ui-lib/src/com/android/car/ui/preference/PreferenceFragment.java
@@ -39,7 +39,11 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.ui.R;
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.CarUi;
 import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
 import com.android.car.ui.utils.CarUiUtils;
 
 import java.util.ArrayDeque;
@@ -58,7 +62,8 @@
  * {@link #setPreferenceScreen(PreferenceScreen)}. These include the preference viewId,
  * defaultValue, and enabled state.
  */
-public abstract class PreferenceFragment extends PreferenceFragmentCompat {
+public abstract class PreferenceFragment extends PreferenceFragmentCompat implements
+        InsetsChangedListener {
 
     private static final String TAG = "CarUiPreferenceFragment";
     private static final String DIALOG_FRAGMENT_TAG =
@@ -67,6 +72,16 @@
     @Override
     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
+
+        ToolbarController baseLayoutToolbar = CarUi.getToolbar(getActivity());
+        if (baseLayoutToolbar != null) {
+            baseLayoutToolbar.setState(Toolbar.State.SUBPAGE);
+            if (getPreferenceScreen() != null) {
+                baseLayoutToolbar.setTitle(getPreferenceScreen().getTitle());
+            }
+        }
+
+        // TODO(b/150230923) remove the code for the old toolbar height change when apps are ready
         final RecyclerView recyclerView = view.findViewById(R.id.recycler_view);
         final Toolbar toolbar = view.findViewById(R.id.toolbar);
         if (recyclerView == null || toolbar == null) {
@@ -85,7 +100,18 @@
         });
 
         recyclerView.setClipToPadding(false);
-        toolbar.setTitle(getPreferenceScreen().getTitle());
+        if (getPreferenceScreen() != null) {
+            toolbar.setTitle(getPreferenceScreen().getTitle());
+        }
+    }
+
+    @Override
+    public void onCarUiInsetsChanged(Insets insets) {
+        View view = requireView();
+        view.requireViewById(R.id.recycler_view)
+                .setPadding(0, insets.getTop(), 0, insets.getBottom());
+        view.getRootView().requireViewById(android.R.id.content)
+                .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
     }
 
     /**
@@ -108,7 +134,7 @@
         }
 
         // check if dialog is already showing
-        if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
+        if (requireFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
             return;
         }
 
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiCheckBoxListItem.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiCheckBoxListItem.java
new file mode 100644
index 0000000..76cbae0
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiCheckBoxListItem.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.recyclerview;
+
+/**
+ * A {@link CarUiContentListItem} that is configured to have a check box action.
+ */
+public class CarUiCheckBoxListItem extends CarUiContentListItem {
+
+    public CarUiCheckBoxListItem() {
+        super(Action.CHECK_BOX);
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiContentListItem.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiContentListItem.java
index 724b639..3fbe006 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiContentListItem.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiContentListItem.java
@@ -30,14 +30,42 @@
     /**
      * Callback to be invoked when the checked state of a list item changed.
      */
-    public interface OnCheckedChangedListener {
+    public interface OnCheckedChangeListener {
         /**
          * Called when the checked state of a list item has changed.
          *
-         * @param item The item whose checked state changed.
+         * @param item      whose checked state changed.
          * @param isChecked new checked state of list item.
          */
-        void onCheckedChanged(CarUiContentListItem item, boolean isChecked);
+        void onCheckedChanged(@NonNull CarUiContentListItem item, boolean isChecked);
+    }
+
+    /**
+     * Callback to be invoked when an item is clicked.
+     */
+    public interface OnClickListener {
+        /**
+         * Called when the item has been clicked.
+         *
+         * @param item whose checked state changed.
+         */
+        void onClick(@NonNull CarUiContentListItem item);
+    }
+
+    public enum IconType {
+        /**
+         * For an icon type of CONTENT, the primary icon is a larger than {@code STANDARD}.
+         */
+        CONTENT,
+        /**
+         * For an icon type of STANDARD, the primary icon is the standard size.
+         */
+        STANDARD,
+        /**
+         * For an icon type of AVATAR, the primary icon is masked to provide an icon with a modified
+         * shape.
+         */
+        AVATAR
     }
 
     /**
@@ -58,8 +86,8 @@
          */
         CHECK_BOX,
         /**
-         * For an action value of CHECK_BOX, a radio button is shown for the action element of the
-         * list item.
+         * For an action value of RADIO_BUTTON, a radio button is shown for the action element of
+         * the list item.
          */
         RADIO_BUTTON,
         /**
@@ -74,13 +102,19 @@
     private CharSequence mTitle;
     private CharSequence mBody;
     private Action mAction;
+    private IconType mPrimaryIconType;
     private boolean mIsActionDividerVisible;
     private boolean mIsChecked;
-    private OnCheckedChangedListener mOnCheckedChangedListener;
+    private boolean mIsEnabled = true;
+    private boolean mIsActivated;
+    private OnClickListener mOnClickListener;
+    private OnCheckedChangeListener mOnCheckedChangeListener;
     private View.OnClickListener mSupplementalIconOnClickListener;
 
-    public CarUiContentListItem() {
-        mAction = Action.NONE;
+
+    public CarUiContentListItem(Action action) {
+        mAction = action;
+        mPrimaryIconType = IconType.STANDARD;
     }
 
     /**
@@ -135,6 +169,54 @@
     }
 
     /**
+     * Returns the primary icon type for the item.
+     */
+    public IconType getPrimaryIconType() {
+        return mPrimaryIconType;
+    }
+
+    /**
+     * Sets the primary icon type for the item.
+     *
+     * @param icon the icon type for the item.
+     */
+    public void setPrimaryIconType(IconType icon) {
+        mPrimaryIconType = icon;
+    }
+
+    /**
+     * Returns {@code true} if the item is activated.
+     */
+    public boolean isActivated() {
+        return mIsActivated;
+    }
+
+    /**
+     * Sets the activated state of the item.
+     *
+     * @param activated the activated state for the item.
+     */
+    public void setActivated(boolean activated) {
+        mIsActivated = activated;
+    }
+
+    /**
+     * Returns {@code true} if the item is enabled.
+     */
+    public boolean isEnabled() {
+        return mIsEnabled;
+    }
+
+    /**
+     * Sets the enabled state of the item.
+     *
+     * @param enabled the enabled state for the item.
+     */
+    public void setEnabled(boolean enabled) {
+        mIsEnabled = enabled;
+    }
+
+    /**
      * Returns {@code true} if the item is checked. Will always return {@code false} when the action
      * type for the item is {@code Action.NONE}.
      */
@@ -148,10 +230,18 @@
      * @param checked the checked state for the item.
      */
     public void setChecked(boolean checked) {
+        if (checked == mIsChecked) {
+            return;
+        }
+
         // Checked state can only be set when action type is checkbox, radio button or switch.
         if (mAction == Action.CHECK_BOX || mAction == Action.SWITCH
                 || mAction == Action.RADIO_BUTTON) {
             mIsChecked = checked;
+
+            if (mOnCheckedChangeListener != null) {
+                mOnCheckedChangeListener.onCheckedChanged(this, mIsChecked);
+            }
         }
     }
 
@@ -179,22 +269,6 @@
     }
 
     /**
-     * Sets the action type for the item.
-     *
-     * @param action the action type for the item.
-     */
-    public void setAction(Action action) {
-        mAction = action;
-
-        // Cannot have checked state be true when there action type is not checkbox, radio button or
-        // switch.
-        if (mAction != Action.CHECK_BOX && mAction != Action.SWITCH
-                && mAction != Action.RADIO_BUTTON) {
-            mIsChecked = false;
-        }
-    }
-
-    /**
      * Returns the supplemental icon for the item.
      */
     @Nullable
@@ -223,20 +297,39 @@
      */
     public void setSupplementalIcon(@Nullable Drawable icon,
             @Nullable View.OnClickListener listener) {
-        mAction = Action.ICON;
-
-        // Cannot have checked state when action type is {@code Action.ICON}.
-        mIsChecked = false;
+        if (mAction != Action.ICON) {
+            throw new IllegalStateException(
+                    "Cannot set supplemental icon on list item that does not have an action of "
+                            + "type ICON");
+        }
 
         mSupplementalIcon = icon;
         mSupplementalIconOnClickListener = listener;
     }
 
-    View.OnClickListener getSupplementalIconOnClickListener() {
+    @Nullable
+    public View.OnClickListener getSupplementalIconOnClickListener() {
         return mSupplementalIconOnClickListener;
     }
 
     /**
+     * Registers a callback to be invoked when the item is clicked.
+     *
+     * @param listener callback to be invoked when item is clicked.
+     */
+    public void setOnItemClickedListener(@Nullable OnClickListener listener) {
+        mOnClickListener = listener;
+    }
+
+    /**
+     * Returns the {@link OnClickListener} registered for this item.
+     */
+    @Nullable
+    public OnClickListener getOnClickListener() {
+        return mOnClickListener;
+    }
+
+    /**
      * Registers a callback to be invoked when the checked state of list item changes.
      *
      * <p>Checked state changes can take place when the action type is {@code Action.SWITCH} or
@@ -244,13 +337,16 @@
      *
      * @param listener callback to be invoked when the checked state shown in the UI changes.
      */
-    public void setOnCheckedChangedListener(
-            @NonNull OnCheckedChangedListener listener) {
-        mOnCheckedChangedListener = listener;
+    public void setOnCheckedChangeListener(
+            @Nullable OnCheckedChangeListener listener) {
+        mOnCheckedChangeListener = listener;
     }
 
+    /**
+     * Returns the {@link OnCheckedChangeListener} registered for this item.
+     */
     @Nullable
-    OnCheckedChangedListener getOnCheckedChangedListener() {
-        return mOnCheckedChangedListener;
+    public OnCheckedChangeListener getOnCheckedChangeListener() {
+        return mOnCheckedChangeListener;
     }
 }
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemAdapter.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
index 1b71411..65a6d40 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
@@ -16,18 +16,22 @@
 
 package com.android.car.ui.recyclerview;
 
+import static com.android.car.ui.utils.CarUiUtils.findViewByRefId;
+
 import android.graphics.drawable.Drawable;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.CheckBox;
+import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.RadioButton;
 import android.widget.Switch;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.ui.R;
@@ -42,17 +46,16 @@
  * <li> Implements {@link CarUiRecyclerView.ItemCap} - defaults to unlimited item count.
  * </ul>
  */
-public class CarUiListItemAdapter extends
-        RecyclerView.Adapter<RecyclerView.ViewHolder> implements
+public class CarUiListItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements
         CarUiRecyclerView.ItemCap {
 
-    private static final int VIEW_TYPE_LIST_ITEM = 1;
-    private static final int VIEW_TYPE_LIST_HEADER = 2;
+    static final int VIEW_TYPE_LIST_ITEM = 1;
+    static final int VIEW_TYPE_LIST_HEADER = 2;
 
-    private List<CarUiListItem> mItems;
+    private List<? extends CarUiListItem> mItems;
     private int mMaxItems = CarUiRecyclerView.ItemCap.UNLIMITED;
 
-    public CarUiListItemAdapter(List<CarUiListItem> items) {
+    public CarUiListItemAdapter(List<? extends CarUiListItem> items) {
         this.mItems = items;
     }
 
@@ -81,7 +84,7 @@
      * appropriate notify method for the adapter.
      */
     @NonNull
-    public List<CarUiListItem> getItems() {
+    public List<? extends CarUiListItem> getItems() {
         return mItems;
     }
 
@@ -149,37 +152,43 @@
      */
     static class ListItemViewHolder extends RecyclerView.ViewHolder {
 
-        private final TextView mTitle;
-        private final TextView mBody;
-        private final ImageView mIcon;
-        private final ViewGroup mIconContainer;
-        private final ViewGroup mActionContainer;
-        private final View mActionDivider;
-        private final Switch mSwitch;
-        private final CheckBox mCheckBox;
-        private final RadioButton mRadioButton;
-        private final ImageView mSupplementalIcon;
-        private final View mTouchInterceptor;
-        private final View mReducedTouchInterceptor;
-
+        final TextView mTitle;
+        final TextView mBody;
+        final ImageView mIcon;
+        final ImageView mContentIcon;
+        final ImageView mAvatarIcon;
+        final ViewGroup mIconContainer;
+        final ViewGroup mActionContainer;
+        final View mActionDivider;
+        final Switch mSwitch;
+        final CheckBox mCheckBox;
+        final RadioButton mRadioButton;
+        final ImageView mSupplementalIcon;
+        final View mTouchInterceptor;
+        final View mReducedTouchInterceptor;
+        final View mActionContainerTouchInterceptor;
 
         ListItemViewHolder(@NonNull View itemView) {
             super(itemView);
-            mTitle = itemView.requireViewById(R.id.title);
-            mBody = itemView.requireViewById(R.id.body);
-            mIcon = itemView.requireViewById(R.id.icon);
-            mIconContainer = itemView.requireViewById(R.id.icon_container);
-            mActionContainer = itemView.requireViewById(R.id.action_container);
-            mActionDivider = itemView.requireViewById(R.id.action_divider);
-            mSwitch = itemView.requireViewById(R.id.switch_widget);
-            mCheckBox = itemView.requireViewById(R.id.checkbox_widget);
-            mRadioButton = itemView.requireViewById(R.id.radio_button_widget);
-            mSupplementalIcon = itemView.requireViewById(R.id.supplemental_icon);
-            mReducedTouchInterceptor = itemView.requireViewById(R.id.reduced_touch_interceptor);
-            mTouchInterceptor = itemView.requireViewById(R.id.touch_interceptor);
+            mTitle = findViewByRefId(itemView, R.id.title);
+            mBody = findViewByRefId(itemView, R.id.body);
+            mIcon = findViewByRefId(itemView, R.id.icon);
+            mContentIcon = findViewByRefId(itemView, R.id.content_icon);
+            mAvatarIcon = findViewByRefId(itemView, R.id.avatar_icon);
+            mIconContainer = findViewByRefId(itemView, R.id.icon_container);
+            mActionContainer = findViewByRefId(itemView, R.id.action_container);
+            mActionDivider = findViewByRefId(itemView, R.id.action_divider);
+            mSwitch = findViewByRefId(itemView, R.id.switch_widget);
+            mCheckBox = findViewByRefId(itemView, R.id.checkbox_widget);
+            mRadioButton = findViewByRefId(itemView, R.id.radio_button_widget);
+            mSupplementalIcon = findViewByRefId(itemView, R.id.supplemental_icon);
+            mReducedTouchInterceptor = findViewByRefId(itemView, R.id.reduced_touch_interceptor);
+            mTouchInterceptor = findViewByRefId(itemView, R.id.touch_interceptor);
+            mActionContainerTouchInterceptor = findViewByRefId(itemView,
+                    R.id.action_container_touch_interceptor);
         }
 
-        private void bind(@NonNull CarUiContentListItem item) {
+        void bind(@NonNull CarUiContentListItem item) {
             CharSequence title = item.getTitle();
             CharSequence body = item.getBody();
             Drawable icon = item.getIcon();
@@ -193,25 +202,46 @@
 
             if (!TextUtils.isEmpty(body)) {
                 mBody.setText(body);
+                mBody.setVisibility(View.VISIBLE);
             } else {
                 mBody.setVisibility(View.GONE);
             }
 
+            mIcon.setVisibility(View.GONE);
+            mContentIcon.setVisibility(View.GONE);
+            mAvatarIcon.setVisibility(View.GONE);
+
             if (icon != null) {
-                mIcon.setImageDrawable(icon);
                 mIconContainer.setVisibility(View.VISIBLE);
+
+                switch (item.getPrimaryIconType()) {
+                    case CONTENT:
+                        mContentIcon.setVisibility(View.VISIBLE);
+                        mContentIcon.setImageDrawable(icon);
+                        break;
+                    case STANDARD:
+                        mIcon.setVisibility(View.VISIBLE);
+                        mIcon.setImageDrawable(icon);
+                        break;
+                    case AVATAR:
+                        mAvatarIcon.setVisibility(View.VISIBLE);
+                        mAvatarIcon.setImageDrawable(icon);
+                        mAvatarIcon.setClipToOutline(true);
+                        break;
+                }
             } else {
                 mIconContainer.setVisibility(View.GONE);
             }
 
             mActionDivider.setVisibility(
                     item.isActionDividerVisible() ? View.VISIBLE : View.GONE);
-
             mSwitch.setVisibility(View.GONE);
             mCheckBox.setVisibility(View.GONE);
             mRadioButton.setVisibility(View.GONE);
             mSupplementalIcon.setVisibility(View.GONE);
 
+            CarUiContentListItem.OnClickListener itemOnClickListener = item.getOnClickListener();
+
             switch (item.getAction()) {
                 case NONE:
                     mActionContainer.setVisibility(View.GONE);
@@ -219,86 +249,35 @@
                     // Display ripple effects across entire item when clicked by using full-sized
                     // touch interceptor.
                     mTouchInterceptor.setVisibility(View.VISIBLE);
+                    mTouchInterceptor.setOnClickListener(v -> {
+                        if (itemOnClickListener != null) {
+                            itemOnClickListener.onClick(item);
+                        }
+                    });
                     mReducedTouchInterceptor.setVisibility(View.GONE);
+                    mActionContainerTouchInterceptor.setVisibility(View.GONE);
                     break;
                 case SWITCH:
-                    mSwitch.setVisibility(View.VISIBLE);
-                    mSwitch.setOnCheckedChangeListener(null);
-                    mSwitch.setChecked(item.isChecked());
-                    mSwitch.setOnCheckedChangeListener(
-                            (buttonView, isChecked) -> {
-                                item.setChecked(isChecked);
-                                CarUiContentListItem.OnCheckedChangedListener itemListener =
-                                        item.getOnCheckedChangedListener();
-                                if (itemListener != null) {
-                                    itemListener.onCheckedChanged(item, isChecked);
-                                }
-                            });
-
-                    // Clicks anywhere on the item should toggle the switch state. Use full touch
-                    // interceptor.
-                    mTouchInterceptor.setVisibility(View.VISIBLE);
-                    mTouchInterceptor.setOnClickListener(v -> mSwitch.toggle());
-                    mReducedTouchInterceptor.setVisibility(View.GONE);
-
-                    mActionContainer.setVisibility(View.VISIBLE);
-                    mActionContainer.setClickable(false);
+                    bindCompoundButton(item, mSwitch, itemOnClickListener);
                     break;
                 case CHECK_BOX:
-                    mCheckBox.setVisibility(View.VISIBLE);
-                    mCheckBox.setOnCheckedChangeListener(null);
-                    mCheckBox.setChecked(item.isChecked());
-                    mCheckBox.setOnCheckedChangeListener(
-                            (buttonView, isChecked) -> {
-                                item.setChecked(isChecked);
-                                CarUiContentListItem.OnCheckedChangedListener itemListener =
-                                        item.getOnCheckedChangedListener();
-                                if (itemListener != null) {
-                                    itemListener.onCheckedChanged(item, isChecked);
-                                }
-                            });
-
-                    // Clicks anywhere on the item should toggle the checkbox state. Use full touch
-                    // interceptor.
-                    mTouchInterceptor.setVisibility(View.VISIBLE);
-                    mTouchInterceptor.setOnClickListener(v -> mCheckBox.toggle());
-                    mReducedTouchInterceptor.setVisibility(View.GONE);
-
-                    mActionContainer.setVisibility(View.VISIBLE);
-                    mActionContainer.setClickable(false);
+                    bindCompoundButton(item, mCheckBox, itemOnClickListener);
                     break;
                 case RADIO_BUTTON:
-                    mRadioButton.setVisibility(View.VISIBLE);
-                    mRadioButton.setOnCheckedChangeListener(null);
-                    mRadioButton.setChecked(item.isChecked());
-                    mRadioButton.setOnCheckedChangeListener(
-                            (buttonView, isChecked) -> {
-                                item.setChecked(isChecked);
-                                CarUiContentListItem.OnCheckedChangedListener itemListener =
-                                        item.getOnCheckedChangedListener();
-                                if (itemListener != null) {
-                                    itemListener.onCheckedChanged(item, isChecked);
-                                }
-                            });
-
-                    // Clicks anywhere on the item should toggle the switch state. Use full touch
-                    // interceptor.
-                    mTouchInterceptor.setVisibility(View.VISIBLE);
-                    mTouchInterceptor.setOnClickListener(v -> mRadioButton.toggle());
-                    mReducedTouchInterceptor.setVisibility(View.GONE);
-
-                    mActionContainer.setVisibility(View.VISIBLE);
-                    mActionContainer.setClickable(false);
+                    bindCompoundButton(item, mRadioButton, itemOnClickListener);
                     break;
                 case ICON:
                     mSupplementalIcon.setVisibility(View.VISIBLE);
                     mSupplementalIcon.setImageDrawable(item.getSupplementalIcon());
                     mActionContainer.setVisibility(View.VISIBLE);
-                    mActionContainer.setOnClickListener(
+                    mActionContainerTouchInterceptor.setOnClickListener(
                             (container) -> {
                                 if (item.getSupplementalIconOnClickListener() != null) {
                                     item.getSupplementalIconOnClickListener().onClick(mIcon);
                                 }
+                                if (itemOnClickListener != null) {
+                                    itemOnClickListener.onClick(item);
+                                }
                             });
 
                     // If the icon has a click listener, use a reduced touch interceptor to create
@@ -307,18 +286,66 @@
                     // click listener, it shouldn't be clickable.
                     if (item.getSupplementalIconOnClickListener() == null) {
                         mTouchInterceptor.setVisibility(View.VISIBLE);
-                        mTouchInterceptor.setOnClickListener(null);
+                        mTouchInterceptor.setOnClickListener(v -> {
+                            if (itemOnClickListener != null) {
+                                itemOnClickListener.onClick(item);
+                            }
+                        });
                         mReducedTouchInterceptor.setVisibility(View.GONE);
-                        mActionContainer.setClickable(false);
+                        mActionContainerTouchInterceptor.setVisibility(View.GONE);
                     } else {
                         mReducedTouchInterceptor.setVisibility(View.VISIBLE);
-                        mReducedTouchInterceptor.setOnClickListener(null);
+                        mReducedTouchInterceptor.setOnClickListener(v -> {
+                            if (itemOnClickListener != null) {
+                                itemOnClickListener.onClick(item);
+                            }
+                        });
+                        mActionContainerTouchInterceptor.setVisibility(View.VISIBLE);
                         mTouchInterceptor.setVisibility(View.GONE);
                     }
                     break;
                 default:
                     throw new IllegalStateException("Unknown secondary action type.");
             }
+
+            itemView.setActivated(item.isActivated());
+            setEnabled(itemView, item.isEnabled());
+        }
+
+        void setEnabled(View view, boolean enabled) {
+            view.setEnabled(enabled);
+            if (view instanceof ViewGroup) {
+                ViewGroup group = (ViewGroup) view;
+
+                for (int i = 0; i < group.getChildCount(); i++) {
+                    setEnabled(group.getChildAt(i), enabled);
+                }
+            }
+        }
+
+        void bindCompoundButton(@NonNull CarUiContentListItem item,
+                @NonNull CompoundButton compoundButton,
+                @Nullable CarUiContentListItem.OnClickListener itemOnClickListener) {
+            compoundButton.setVisibility(View.VISIBLE);
+            compoundButton.setOnCheckedChangeListener(null);
+            compoundButton.setChecked(item.isChecked());
+            compoundButton.setOnCheckedChangeListener(
+                    (buttonView, isChecked) -> item.setChecked(isChecked));
+
+            // Clicks anywhere on the item should toggle the checkbox state. Use full touch
+            // interceptor.
+            mTouchInterceptor.setVisibility(View.VISIBLE);
+            mTouchInterceptor.setOnClickListener(v -> {
+                compoundButton.toggle();
+                if (itemOnClickListener != null) {
+                    itemOnClickListener.onClick(item);
+                }
+            });
+            mReducedTouchInterceptor.setVisibility(View.GONE);
+            mActionContainerTouchInterceptor.setVisibility(View.GONE);
+
+            mActionContainer.setVisibility(View.VISIBLE);
+            mActionContainer.setClickable(false);
         }
     }
 
@@ -332,8 +359,8 @@
 
         HeaderViewHolder(@NonNull View itemView) {
             super(itemView);
-            mTitle = itemView.requireViewById(R.id.title);
-            mBody = itemView.requireViewById(R.id.body);
+            mTitle = findViewByRefId(itemView, R.id.title);
+            mBody = findViewByRefId(itemView, R.id.body);
         }
 
         private void bind(@NonNull CarUiHeaderListItem item) {
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java
deleted file mode 100644
index 5575dee..0000000
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java
+++ /dev/null
@@ -1,170 +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.ui.recyclerview;
-
-import android.content.Context;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.car.ui.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A {@link LinearLayoutManager} implementation which provides a fixed scrollbar when used with
- * viewholders that vary in height. This layout manager is only compatible with {@link
- * CarUiListItemAdapter}.
- */
-public class CarUiListItemLayoutManager extends LinearLayoutManager {
-
-    private final int mContentItemSize;
-    private final int mHeaderItemSize;
-
-    private List<Integer> mListItemHeights;
-    private CarUiListItemAdapter mAdapter;
-
-    public CarUiListItemLayoutManager(@NonNull Context context) {
-        super(context);
-        mContentItemSize = (int) context.getResources().getDimension(
-                R.dimen.car_ui_list_item_height);
-        mHeaderItemSize = (int) context.getResources().getDimension(
-                R.dimen.car_ui_list_item_header_height);
-
-        setSmoothScrollbarEnabled(false);
-    }
-
-    @Override
-    public void onAttachedToWindow(RecyclerView recyclerView) {
-        super.onAttachedToWindow(recyclerView);
-        populateHeightMap(recyclerView.getAdapter());
-    }
-
-    @Override
-    public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
-        super.onAdapterChanged(oldAdapter, newAdapter);
-        populateHeightMap(newAdapter);
-    }
-
-    @Override
-    public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart, int itemCount) {
-        super.onItemsAdded(recyclerView, positionStart, itemCount);
-        populateHeightMap(recyclerView.getAdapter());
-    }
-
-    @Override
-    public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart,
-            int itemCount) {
-        super.onItemsRemoved(recyclerView, positionStart, itemCount);
-        populateHeightMap(recyclerView.getAdapter());
-    }
-
-    @Override
-    public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart,
-            int itemCount) {
-        super.onItemsUpdated(recyclerView, positionStart, itemCount);
-        populateHeightMap(recyclerView.getAdapter());
-    }
-
-    @Override
-    public void onItemsChanged(@NonNull RecyclerView recyclerView) {
-        super.onItemsChanged(recyclerView);
-        populateHeightMap(recyclerView.getAdapter());
-    }
-
-    @Override
-    public int computeVerticalScrollExtent(RecyclerView.State state) {
-        final int count = getChildCount();
-        return count > 0 ? mContentItemSize * 3 : 0;
-    }
-
-    @Override
-    public int computeVerticalScrollRange(RecyclerView.State state) {
-        return Math.max(mListItemHeights.get(mListItemHeights.size() - 1), 0);
-    }
-
-    @Override
-    public int computeVerticalScrollOffset(RecyclerView.State state) {
-        if (getChildCount() <= 0) {
-            return 0;
-        }
-
-        int firstPos = findFirstVisibleItemPosition();
-        if (firstPos == RecyclerView.NO_POSITION) {
-            return 0;
-        }
-
-        View view = findViewByPosition(firstPos);
-        if (view == null) {
-            return 0;
-        }
-
-        final int top = getDecoratedTop(view);
-        final int height = getDecoratedMeasuredHeight(view);
-
-        int heightOfScreen;
-        if (height <= 0) {
-            heightOfScreen = 0;
-        } else {
-            CarUiListItem item = mAdapter.getItems().get(firstPos);
-            if (item instanceof CarUiContentListItem) {
-                heightOfScreen = Math.abs(mContentItemSize * top / height);
-            } else if (item instanceof CarUiHeaderListItem) {
-                heightOfScreen = Math.abs(mHeaderItemSize * top / height);
-            } else {
-                throw new IllegalStateException("Unknown list item type.");
-            }
-        }
-
-        return mListItemHeights.get(firstPos) + heightOfScreen;
-    }
-
-    /**
-     * Populates an internal list of cumulative heights at each position for the list to be laid out
-     * for the adapter parameter.
-     *
-     * @param adapter the action type for the item.
-     */
-    private void populateHeightMap(RecyclerView.Adapter adapter) {
-        if (!(adapter instanceof CarUiListItemAdapter)) {
-            throw new IllegalStateException(
-                    "Cannot use CarUiListItemLayoutManager with an adapter that is not of type "
-                            + "CarUiListItemAdapter");
-        }
-
-        mAdapter = (CarUiListItemAdapter) adapter;
-        List<CarUiListItem> itemList = mAdapter.getItems();
-        mListItemHeights = new ArrayList<>();
-
-        int cumulativeHeight = 0;
-        mListItemHeights.add(cumulativeHeight);
-        for (CarUiListItem item : itemList) {
-            if (item instanceof CarUiContentListItem) {
-                cumulativeHeight += mContentItemSize;
-                mListItemHeights.add(cumulativeHeight);
-            } else if (item instanceof CarUiHeaderListItem) {
-                cumulativeHeight += mHeaderItemSize;
-                mListItemHeights.add(cumulativeHeight);
-            } else {
-                throw new IllegalStateException("Unknown CarUiListItem type");
-            }
-        }
-    }
-}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRadioButtonListItem.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRadioButtonListItem.java
new file mode 100644
index 0000000..b6f3043
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRadioButtonListItem.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.recyclerview;
+
+/**
+ * A {@link CarUiContentListItem} that is configured to have a radio button action.
+ */
+public class CarUiRadioButtonListItem extends CarUiContentListItem {
+
+    public CarUiRadioButtonListItem() {
+        super(Action.RADIO_BUTTON);
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRadioButtonListItemAdapter.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRadioButtonListItemAdapter.java
new file mode 100644
index 0000000..ed248c1
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRadioButtonListItemAdapter.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.ui.recyclerview;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+
+import java.util.List;
+
+/**
+ * Adapter for {@link CarUiRecyclerView} to display {@link CarUiRadioButtonListItem}. This adapter
+ * allows for at most one item to be selected at a time.
+ *
+ * <ul>
+ * <li> Implements {@link CarUiRecyclerView.ItemCap} - defaults to unlimited item count.
+ * </ul>
+ */
+public class CarUiRadioButtonListItemAdapter extends CarUiListItemAdapter {
+
+    private int mSelectedIndex = -1;
+
+    public CarUiRadioButtonListItemAdapter(List<CarUiRadioButtonListItem> items) {
+        super(items);
+        for (int i = 0; i < items.size(); i++) {
+            CarUiRadioButtonListItem item = items.get(i);
+            if (item.isChecked() && mSelectedIndex >= 0) {
+                throw new IllegalStateException(
+                        "At most one item in a CarUiRadioButtonListItemAdapter can be checked");
+            }
+
+            if (item.isChecked()) {
+                mSelectedIndex = i;
+            }
+        }
+    }
+
+    @NonNull
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(
+            @NonNull ViewGroup parent, int viewType) {
+        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+
+        if (viewType == VIEW_TYPE_LIST_ITEM) {
+            return new RadioButtonListItemViewHolder(
+                    inflater.inflate(R.layout.car_ui_list_item, parent, false));
+        }
+        return super.onCreateViewHolder(parent, viewType);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+        if (holder.getItemViewType() == VIEW_TYPE_LIST_ITEM) {
+            if (!(holder instanceof RadioButtonListItemViewHolder)) {
+                throw new IllegalStateException("Incorrect view holder type for list item.");
+            }
+
+            CarUiListItem item = getItems().get(position);
+            if (!(item instanceof CarUiRadioButtonListItem)) {
+                throw new IllegalStateException(
+                        "Expected item to be bound to viewholder to be instance of "
+                                + "CarUiRadioButtonListItem.");
+            }
+
+            RadioButtonListItemViewHolder actualHolder = ((RadioButtonListItemViewHolder) holder);
+            actualHolder.bind((CarUiRadioButtonListItem) item);
+            actualHolder.setOnCheckedChangeListener(isChecked -> {
+                if (isChecked && mSelectedIndex >= 0) {
+                    CarUiRadioButtonListItem previousSelectedItem =
+                            (CarUiRadioButtonListItem) getItems().get(mSelectedIndex);
+                    previousSelectedItem.setChecked(false);
+                    notifyItemChanged(mSelectedIndex);
+                }
+
+                if (isChecked) {
+                    mSelectedIndex = position;
+                    CarUiRadioButtonListItem currentSelectedItem =
+                            (CarUiRadioButtonListItem) getItems().get(mSelectedIndex);
+                    currentSelectedItem.setChecked(true);
+                    notifyItemChanged(mSelectedIndex);
+                }
+            });
+
+        } else {
+            super.onBindViewHolder(holder, position);
+        }
+    }
+
+    static class RadioButtonListItemViewHolder extends ListItemViewHolder {
+        /**
+         * Callback to be invoked when the checked state of a {@link RadioButtonListItemViewHolder}
+         * changed.
+         */
+        public interface OnCheckedChangeListener {
+            /**
+             * Called when the checked state of a {@link RadioButtonListItemViewHolder} has changed.
+             *
+             * @param isChecked new checked state of list item.
+             */
+            void onCheckedChanged(boolean isChecked);
+        }
+
+        @Nullable
+        private OnCheckedChangeListener mListener;
+
+        RadioButtonListItemViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        void setOnCheckedChangeListener(@Nullable OnCheckedChangeListener listener) {
+            mListener = listener;
+        }
+
+        @Override
+        void bind(@NonNull CarUiContentListItem item) {
+            super.bind(item);
+            mRadioButton.setOnCheckedChangeListener(
+                    (buttonView, isChecked) -> {
+                        item.setChecked(isChecked);
+                        if (mListener != null) {
+                            mListener.onCheckedChanged(isChecked);
+                        }
+                    });
+        }
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
index 0b608a2..dec9080 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -15,25 +15,25 @@
  */
 package com.android.car.ui.recyclerview;
 
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
 import android.content.res.TypedArray;
-import android.os.Parcel;
-import android.os.Parcelable;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
-import android.util.SparseArray;
-import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
 import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -50,93 +50,38 @@
 import java.lang.annotation.Retention;
 
 /**
- * View that extends a {@link RecyclerView} and creates a nested {@code RecyclerView} which could
- * potentially include a scrollbar that has page up and down arrows. Interaction with this view is
- * similar to a {@code RecyclerView} as it takes the same adapter and the layout manager.
+ * View that extends a {@link RecyclerView} and wraps itself into a {@link LinearLayout} which
+ * could potentially include a scrollbar that has page up and down arrows. Interaction with this
+ * view is similar to a {@code RecyclerView} as it takes the same adapter and the layout manager.
  */
 public final class CarUiRecyclerView extends RecyclerView implements
         Toolbar.OnHeightChangedListener {
 
-    private static final boolean DEBUG = false;
     private static final String TAG = "CarUiRecyclerView";
 
-    private final CarUxRestrictionsUtil mCarUxRestrictionsUtil;
-    private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener;
+    private final UxRestrictionChangedListener mListener = new UxRestrictionChangedListener();
 
+    private CarUxRestrictionsUtil mCarUxRestrictionsUtil;
     private boolean mScrollBarEnabled;
-    private int mScrollBarContainerWidth;
-    @ScrollBarPosition
-    private int mScrollBarPosition;
-    private boolean mScrollBarAboveRecyclerView;
     private String mScrollBarClass;
     private boolean mFullyInitialized;
     private float mScrollBarPaddingStart;
     private float mScrollBarPaddingEnd;
-    private Context mContext;
 
-    @Gutter
-    private int mGutter;
-    private int mGutterSize;
-    @VisibleForTesting
-    RecyclerView mNestedRecyclerView;
-    private boolean mIsNestedRecyclerViewInitialized;
-    private Adapter<?> mAdapter;
     private ScrollBar mScrollBar;
     private int mInitialTopPadding;
 
     private GridOffsetItemDecoration mOffsetItemDecoration;
     private GridDividerItemDecoration mDividerItemDecoration;
     @CarUiRecyclerViewLayout
-    int mCarUiRecyclerViewLayout;
+    private int mCarUiRecyclerViewLayout;
     private int mNumOfColumns;
+    private boolean mInstallingExtScrollBar = false;
+    private int mContainerVisibility = View.VISIBLE;
+    private LinearLayout mContainer;
 
     /**
-     * The possible values for @{link #setGutter}. The default value is actually {@link
-     * CarUiRecyclerView.Gutter#BOTH}.
-     */
-    @IntDef({
-            Gutter.NONE,
-            Gutter.START,
-            Gutter.END,
-            Gutter.BOTH,
-    })
-    @Retention(SOURCE)
-    public @interface Gutter {
-        /**
-         * No gutter on either side of the list items. The items will span the full width of the
-         * RecyclerView
-         */
-        int NONE = 0;
-
-        /** Include a gutter only on the start side (that is, the same side as the scroll bar). */
-        int START = 1;
-
-        /** Include a gutter only on the end side (that is, the opposite side of the scroll bar). */
-        int END = 2;
-
-        /** Include a gutter on both sides of the list items. This is the default behaviour. */
-        int BOTH = 3;
-    }
-
-    /**
-     * The possible values for setScrollbarPosition. The default value is actually {@link
-     * CarUiRecyclerView.ScrollBarPosition#START}.
-     */
-    @IntDef({
-            ScrollBarPosition.START,
-            ScrollBarPosition.END,
-    })
-    @Retention(SOURCE)
-    public @interface ScrollBarPosition {
-        /** Position the scrollbar to the left of the screen. This is default. */
-        int START = 0;
-
-        /** Position scrollbar to the right of the screen. */
-        int END = 1;
-    }
-
-    /**
-     * The possible values for setScrollbarPosition. The default value is actually {@link
+     * The possible values for setScrollBarPosition. The default value is actually {@link
      * CarUiRecyclerViewLayout#LINEAR}.
      */
     @IntDef({
@@ -145,10 +90,13 @@
     })
     @Retention(SOURCE)
     public @interface CarUiRecyclerViewLayout {
-        /** Position the scrollbar to the left of the screen. This is default. */
+        /**
+         * Arranges items either horizontally in a single row or vertically in a single column.
+         * This is default.
+         */
         int LINEAR = 0;
 
-        /** Position scrollbar to the right of the screen. */
+        /** Arranges items in a Grid. */
         int GRID = 2;
     }
 
@@ -168,7 +116,10 @@
      * }</pre>
      */
     public interface ItemCap {
-        /** A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit. */
+
+        /**
+         * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit.
+         */
         int UNLIMITED = -1;
 
         /**
@@ -179,47 +130,6 @@
         void setMaxItems(int maxItems);
     }
 
-    /**
-     * Custom layout manager for the outer recyclerview. Since paddings should be applied by the
-     * inner
-     * recycler view within its bounds, this layout manager should always have 0 padding.
-     */
-    private static class CarUiRecyclerViewLayoutManager extends LinearLayoutManager {
-        CarUiRecyclerViewLayoutManager(Context context) {
-            super(context);
-        }
-
-        @Override
-        public int getPaddingTop() {
-            return 0;
-        }
-
-        @Override
-        public int getPaddingBottom() {
-            return 0;
-        }
-
-        @Override
-        public int getPaddingStart() {
-            return 0;
-        }
-
-        @Override
-        public int getPaddingEnd() {
-            return 0;
-        }
-
-        @Override
-        public boolean canScrollHorizontally() {
-            return false;
-        }
-
-        @Override
-        public boolean canScrollVertically() {
-            return false;
-        }
-    }
-
     public CarUiRecyclerView(@NonNull Context context) {
         this(context, null);
     }
@@ -228,16 +138,14 @@
         this(context, attrs, R.attr.carUiRecyclerViewStyle);
     }
 
-    public CarUiRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
+    public CarUiRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyle) {
         super(context, attrs, defStyle);
-
-        mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
-        mListener = this::updateCarUxRestrictions;
-
         init(context, attrs, defStyle);
     }
 
     private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+        mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
         TypedArray a = context.obtainStyledAttributes(
                 attrs,
                 R.styleable.CarUiRecyclerView,
@@ -247,14 +155,6 @@
         mScrollBarEnabled = context.getResources().getBoolean(R.bool.car_ui_scrollbar_enable);
         mFullyInitialized = false;
 
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView =
-                    new RecyclerView(new ContextThemeWrapper(context,
-                            R.style.Widget_CarUi_CarUiRecyclerView_NestedRecyclerView), attrs,
-                            R.style.Widget_CarUi_CarUiRecyclerView_NestedRecyclerView);
-            mNestedRecyclerView.setVerticalScrollBarEnabled(false);
-        }
-
         mScrollBarPaddingStart =
                 context.getResources().getDimension(R.dimen.car_ui_scrollbar_padding_start);
         mScrollBarPaddingEnd =
@@ -287,7 +187,7 @@
 
             addItemDecoration(topOffsetItemDecoration);
             addItemDecoration(bottomOffsetItemDecoration);
-            setLayoutManager(new LinearLayoutManager(mContext));
+            setLayoutManager(new LinearLayoutManager(getContext()));
         } else {
             int gridTopOffset =
                     a.getInteger(R.styleable.CarUiRecyclerView_startOffset, /* defValue= */ 0);
@@ -313,7 +213,7 @@
 
             addItemDecoration(mOffsetItemDecoration);
             addItemDecoration(bottomOffsetItemDecoration);
-            setLayoutManager(new GridLayoutManager(mContext, mNumOfColumns));
+            setLayoutManager(new GridLayoutManager(getContext(), mNumOfColumns));
             setNumOfColumns(mNumOfColumns);
         }
 
@@ -323,62 +223,15 @@
             return;
         }
 
-        super.setLayoutManager(new CarUiRecyclerViewLayoutManager(context));
-        super.setAdapter(new CarUiRecyclerViewAdapter());
-        super.setNestedScrollingEnabled(false);
-        super.setClipToPadding(false);
-
-        // Gutter
-        mGutter = context.getResources().getInteger(R.integer.car_ui_scrollbar_gutter);
-        mGutterSize = getResources().getDimensionPixelSize(R.dimen.car_ui_scrollbar_margin);
-
-        mScrollBarContainerWidth =
-                (int) context.getResources().getDimension(
-                        R.dimen.car_ui_scrollbar_container_width);
-
-        mScrollBarPosition = context.getResources().getInteger(
-                R.integer.car_ui_scrollbar_position);
-
-        mScrollBarAboveRecyclerView =
-                context.getResources().getBoolean(R.bool.car_ui_scrollbar_above_recycler_view);
         mScrollBarClass = context.getResources().getString(R.string.car_ui_scrollbar_component);
         a.recycle();
-        this.mContext = context;
-        // Apply inner RV layout changes after the layout has been calculated for this view.
         this.getViewTreeObserver()
-                .addOnGlobalLayoutListener(
-                        new OnGlobalLayoutListener() {
-                            @Override
-                            public void onGlobalLayout() {
-                                // View holder layout is still pending.
-                                if (CarUiRecyclerView.this.findViewHolderForAdapterPosition(0)
-                                        == null) {
-                                    return;
-                                }
-                                CarUiRecyclerView.this.getViewTreeObserver()
-                                        .removeOnGlobalLayoutListener(this);
-                                initNestedRecyclerView();
-                                setNestedViewLayout();
-
-                                mNestedRecyclerView.setHasFixedSize(true);
-                                mNestedRecyclerView
-                                        .getViewTreeObserver()
-                                        .addOnGlobalLayoutListener(
-                                                new OnGlobalLayoutListener() {
-                                                    @Override
-                                                    public void onGlobalLayout() {
-                                                        mNestedRecyclerView
-                                                                .getViewTreeObserver()
-                                                                .removeOnGlobalLayoutListener(this);
-                                                        createScrollBarFromConfig();
-                                                        if (mInitialTopPadding == 0) {
-                                                            mInitialTopPadding = getPaddingTop();
-                                                        }
-                                                        mFullyInitialized = true;
-                                                    }
-                                                });
-                            }
-                        });
+                .addOnGlobalLayoutListener(() -> {
+                    if (mInitialTopPadding == 0) {
+                        mInitialTopPadding = getPaddingTop();
+                    }
+                    mFullyInitialized = true;
+                });
     }
 
     @Override
@@ -388,19 +241,17 @@
     }
 
     /**
-     * Returns {@code true} if the {@CarUiRecyclerView} is fully drawn. Using a global layout
-     * mListener
-     * may not necessarily signify that this view is fully drawn (i.e. when the scrollbar is
-     * enabled).
-     * This is because the inner views (scrollbar and inner recycler view) are drawn after the
-     * outer
-     * views are finished.
+     * Returns {@code true} if the {@link CarUiRecyclerView} is fully drawn. Using a global layout
+     * mListener may not necessarily signify that this view is fully drawn (i.e. when the scrollbar
+     * is enabled).
      */
     public boolean fullyInitialized() {
         return mFullyInitialized;
     }
 
-    /** Sets the number of columns in which grid needs to be divided. */
+    /**
+     * Sets the number of columns in which grid needs to be divided.
+     */
     public void setNumOfColumns(int numberOfColumns) {
         mNumOfColumns = numberOfColumns;
         if (mOffsetItemDecoration != null) {
@@ -411,289 +262,60 @@
         }
     }
 
-    /**
-     * Returns the {@link LayoutManager} for the {@link RecyclerView} displaying the content.
-     *
-     * <p>In cases where the scroll bar is visible and the nested {@link RecyclerView} is displaying
-     * content, {@link #getLayoutManager()} cannot be used because it returns the {@link
-     * LayoutManager} of the outer {@link RecyclerView}. {@link #getLayoutManager()} could not be
-     * overridden to return the effective manager due to interference with accessibility node tree
-     * traversal.
-     */
-    @Nullable
-    public LayoutManager getEffectiveLayoutManager() {
-        if (mScrollBarEnabled) {
-            return mNestedRecyclerView.getLayoutManager();
-        }
-        return super.getLayoutManager();
-    }
-
-    /**
-     * Refer to {@link CarUiRecyclerView#getEffectiveLayoutManager()} for usage in applications.
-     */
     @Override
-    public LayoutManager getLayoutManager() {
-        return super.getLayoutManager();
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        mContainerVisibility = visibility;
+        if (mContainer != null) {
+            mContainer.setVisibility(visibility);
+        }
     }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         mCarUxRestrictionsUtil.register(mListener);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        mCarUxRestrictionsUtil.unregister(mListener);
-    }
-
-    private void updateCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
-        // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
-        if (!(mAdapter instanceof ItemCap)) {
+        if (mInstallingExtScrollBar || !mScrollBarEnabled) {
             return;
         }
-
-        int maxItems = ItemCap.UNLIMITED;
-        if ((carUxRestrictions.getActiveRestrictions()
-                & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT)
-                != 0) {
-            maxItems = carUxRestrictions.getMaxCumulativeContentItems();
-        }
-
-        int originalCount = mAdapter.getItemCount();
-        ((ItemCap) mAdapter).setMaxItems(maxItems);
-        int newCount = mAdapter.getItemCount();
-
-        if (newCount == originalCount) {
-            return;
-        }
-
-        if (newCount < originalCount) {
-            mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
-        } else {
-            mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
-        }
+        // When CarUiRV is detached from the current parent and attached to the container with
+        // the scrollBar, onAttachedToWindow() will get called immediately when attaching the
+        // CarUiRV to the container. This flag will help us keep track of this state and avoid
+        // recursion. We also want to reset the state of this flag as soon as the container is
+        // successfully attached to the CarUiRV's original parent.
+        mInstallingExtScrollBar = true;
+        installExternalScrollBar();
+        mInstallingExtScrollBar = false;
     }
 
-    @Override
-    public void setClipToPadding(boolean clipToPadding) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setClipToPadding(clipToPadding);
-        } else {
-            super.setClipToPadding(clipToPadding);
-        }
+    /**
+     * This method will detach the current recycler view from its parent and attach it to the
+     * container which is a LinearLayout. Later the entire container is attached to the
+     * parent where the recycler view was set with the same layout params.
+     */
+    private void installExternalScrollBar() {
+        ViewGroup parent = (ViewGroup) getParent();
+        mContainer = new LinearLayout(getContext());
+        LayoutInflater inflater = LayoutInflater.from(getContext());
+        inflater.inflate(R.layout.car_ui_recycler_view, mContainer, true);
+
+        mContainer.setLayoutParams(getLayoutParams());
+        mContainer.setVisibility(mContainerVisibility);
+        int index = parent.indexOfChild(this);
+        parent.removeView(this);
+        ((FrameLayout) requireViewByRefId(mContainer, R.id.car_ui_recycler_view))
+                .addView(this,
+                        new FrameLayout.LayoutParams(
+                                ViewGroup.LayoutParams.MATCH_PARENT,
+                                ViewGroup.LayoutParams.MATCH_PARENT));
+        setVerticalScrollBarEnabled(false);
+        setHorizontalScrollBarEnabled(false);
+        parent.addView(mContainer, index);
+
+        createScrollBarFromConfig(requireViewByRefId(mContainer, R.id.car_ui_scroll_bar));
     }
 
-    @SuppressWarnings("rawtypes")
-    @Override
-    public void setAdapter(@Nullable Adapter adapter) {
-
-        this.mAdapter = adapter;
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setAdapter(adapter);
-        } else {
-            super.setAdapter(adapter);
-        }
-    }
-
-    @Nullable
-    @Override
-    public Adapter<?> getAdapter() {
-        if (mScrollBarEnabled) {
-            return mNestedRecyclerView.getAdapter();
-        }
-        return super.getAdapter();
-    }
-
-    @Override
-    public void setLayoutManager(@Nullable LayoutManager layout) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setLayoutManager(layout);
-        } else {
-            super.setLayoutManager(layout);
-        }
-    }
-
-    @Override
-    public void setOnScrollChangeListener(OnScrollChangeListener l) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setOnScrollChangeListener(l);
-        } else {
-            super.setOnScrollChangeListener(l);
-        }
-    }
-
-    @Override
-    public void setVerticalFadingEdgeEnabled(boolean verticalFadingEdgeEnabled) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
-        } else {
-            super.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
-        }
-    }
-
-    @Override
-    public void setFadingEdgeLength(int length) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setFadingEdgeLength(length);
-        } else {
-            super.setFadingEdgeLength(length);
-        }
-    }
-
-    @Override
-    public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.addItemDecoration(decor, index);
-        } else {
-            super.addItemDecoration(decor, index);
-        }
-    }
-
-    @Override
-    public void addItemDecoration(@NonNull ItemDecoration decor) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.addItemDecoration(decor);
-        } else {
-            super.addItemDecoration(decor);
-        }
-    }
-
-    @Override
-    public void setItemAnimator(@Nullable ItemAnimator animator) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setItemAnimator(animator);
-        } else {
-            super.setItemAnimator(animator);
-        }
-    }
-
-    @Override
-    public void setPadding(int left, int top, int right, int bottom) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setPadding(left, top, right, bottom);
-            if (mScrollBar != null) {
-                mScrollBar.requestLayout();
-            }
-        } else {
-            super.setPadding(left, top, right, bottom);
-        }
-    }
-
-    @Override
-    public void setPaddingRelative(int start, int top, int end, int bottom) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setPaddingRelative(start, top, end, bottom);
-            if (mScrollBar != null) {
-                mScrollBar.requestLayout();
-            }
-        } else {
-            super.setPaddingRelative(start, top, end, bottom);
-        }
-    }
-
-    @Override
-    public ViewHolder findViewHolderForLayoutPosition(int position) {
-        if (mScrollBarEnabled) {
-            return mNestedRecyclerView.findViewHolderForLayoutPosition(position);
-        } else {
-            return super.findViewHolderForLayoutPosition(position);
-        }
-    }
-
-    @Override
-    public ViewHolder findViewHolderForAdapterPosition(int position) {
-        if (mScrollBarEnabled && mIsNestedRecyclerViewInitialized) {
-            return mNestedRecyclerView.findViewHolderForAdapterPosition(position);
-        } else {
-            return super.findViewHolderForAdapterPosition(position);
-        }
-    }
-
-    @Override
-    public ViewHolder findContainingViewHolder(View view) {
-        if (mScrollBarEnabled) {
-            return mNestedRecyclerView.findContainingViewHolder(view);
-        } else {
-            return super.findContainingViewHolder(view);
-        }
-    }
-
-    @Override
-    @Nullable
-    public View findChildViewUnder(float x, float y) {
-        if (mScrollBarEnabled) {
-            return mNestedRecyclerView.findChildViewUnder(x, y);
-        } else {
-            return super.findChildViewUnder(x, y);
-        }
-    }
-
-    @Override
-    public void addOnScrollListener(@NonNull OnScrollListener listener) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.addOnScrollListener(listener);
-        } else {
-            super.addOnScrollListener(listener);
-        }
-    }
-
-    @Override
-    public void removeOnScrollListener(@NonNull OnScrollListener listener) {
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.removeOnScrollListener(listener);
-        } else {
-            super.removeOnScrollListener(listener);
-        }
-    }
-
-    @Override
-    public int getPaddingStart() {
-        return mScrollBarEnabled ? mNestedRecyclerView.getPaddingStart() : super.getPaddingStart();
-    }
-
-    @Override
-    public int getPaddingEnd() {
-        return mScrollBarEnabled ? mNestedRecyclerView.getPaddingEnd() : super.getPaddingEnd();
-    }
-
-    @Override
-    public int getPaddingTop() {
-        return mScrollBarEnabled ? mNestedRecyclerView.getPaddingTop() : super.getPaddingTop();
-    }
-
-    @Override
-    public int getPaddingBottom() {
-        return mScrollBarEnabled ? mNestedRecyclerView.getPaddingBottom()
-                : super.getPaddingBottom();
-    }
-
-    @Override
-    public void setVisibility(int visibility) {
-        super.setVisibility(visibility);
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.setVisibility(visibility);
-        }
-    }
-
-    private void initNestedRecyclerView() {
-        CarUiRecyclerViewAdapter.NestedRowViewHolder vh =
-                (CarUiRecyclerViewAdapter.NestedRowViewHolder)
-                        this.findViewHolderForAdapterPosition(0);
-        if (vh == null) {
-            throw new Error("Outer RecyclerView failed to initialize.");
-        }
-
-        vh.frameLayout.addView(mNestedRecyclerView);
-        mIsNestedRecyclerViewInitialized = true;
-    }
-
-    private void createScrollBarFromConfig() {
-        if (DEBUG) {
-            Log.d(TAG, "createScrollBarFromConfig");
-        }
-
+    private void createScrollBarFromConfig(View scrollView) {
         Class<?> cls;
         try {
             cls = !TextUtils.isEmpty(mScrollBarClass)
@@ -708,15 +330,15 @@
             throw andLog("Error creating scroll bar component: " + mScrollBarClass, t);
         }
 
-        mScrollBar.initialize(
-                mNestedRecyclerView, mScrollBarContainerWidth, mScrollBarPosition,
-                mScrollBarAboveRecyclerView);
+        mScrollBar.initialize(this, scrollView);
 
         mScrollBar.setPadding((int) mScrollBarPaddingStart, (int) mScrollBarPaddingEnd);
+    }
 
-        if (DEBUG) {
-            Log.d(TAG, "started " + mScrollBar.getClass().getSimpleName());
-        }
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mCarUxRestrictionsUtil.unregister(mListener);
     }
 
     /**
@@ -735,48 +357,12 @@
     }
 
     /**
-     * Calls {@link #layout(int, int, int, int)} for both this RecyclerView and the nested one.
+     * @deprecated use {#getLayoutManager()}
      */
-    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    public void layoutBothForTesting(int l, int t, int r, int b) {
-        super.layout(l, t, r, b);
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.layout(l, t, r, b);
-        }
-    }
-
-    /**
-     * Set the nested view's layout to the specified value.
-     *
-     * <p>The mGutter is the space to the start/end of the list view items and will be equal in size
-     * to
-     * the scroll bars. By default, there is a mGutter to both the left and right of the list view
-     * items, to account for the scroll bar.
-     */
-    private void setNestedViewLayout() {
-        int startMargin = 0;
-        int endMargin = 0;
-        if ((mGutter & Gutter.START) != 0) {
-            startMargin = mGutterSize;
-        }
-        if ((mGutter & Gutter.END) != 0) {
-            endMargin = mGutterSize;
-        }
-
-        MarginLayoutParams layoutParams =
-                (MarginLayoutParams) mNestedRecyclerView.getLayoutParams();
-
-        layoutParams.setMarginStart(startMargin);
-        layoutParams.setMarginEnd(endMargin);
-
-        layoutParams.height = LayoutParams.MATCH_PARENT;
-        layoutParams.width = super.getLayoutManager().getWidth() - startMargin - endMargin;
-        // requestLayout() isn't sufficient because we also need to resolveLayoutParams().
-        mNestedRecyclerView.setLayoutParams(layoutParams);
-
-        // If there's a mGutter, set ClipToPadding to false so that CardView's shadow will still
-        // appear outside of the padding.
-        mNestedRecyclerView.setClipToPadding(startMargin == 0 && endMargin == 0);
+    @Nullable
+    @Deprecated
+    public LayoutManager getEffectiveLayoutManager() {
+        return super.getLayoutManager();
     }
 
     private static RuntimeException andLog(String msg, Throwable t) {
@@ -784,61 +370,38 @@
         throw new RuntimeException(msg, t);
     }
 
-    @Override
-    public Parcelable onSaveInstanceState() {
-        Parcelable superState = super.onSaveInstanceState();
-        SavedState ss = new SavedState(superState);
-        if (mScrollBarEnabled) {
-            mNestedRecyclerView.saveHierarchyState(ss.mNestedRecyclerViewState);
-        }
-        return ss;
-    }
+    private class UxRestrictionChangedListener implements
+            CarUxRestrictionsUtil.OnUxRestrictionsChangedListener {
 
-    @Override
-    public void onRestoreInstanceState(Parcelable state) {
-        if (!(state instanceof SavedState)) {
-            Log.w(TAG, "onRestoreInstanceState called with an unsupported state");
-            super.onRestoreInstanceState(state);
-        } else {
-            SavedState ss = (SavedState) state;
-            super.onRestoreInstanceState(ss.getSuperState());
-            if (mScrollBarEnabled) {
-                mNestedRecyclerView.restoreHierarchyState(ss.mNestedRecyclerViewState);
+        @Override
+        public void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions) {
+            Adapter<?> adapter = getAdapter();
+            // If the adapter does not implement ItemCap, then the max items on it cannot be
+            // updated.
+            if (!(adapter instanceof ItemCap)) {
+                return;
+            }
+
+            int maxItems = ItemCap.UNLIMITED;
+            if ((carUxRestrictions.getActiveRestrictions()
+                    & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT)
+                    != 0) {
+                maxItems = carUxRestrictions.getMaxCumulativeContentItems();
+            }
+
+            int originalCount = adapter.getItemCount();
+            ((ItemCap) adapter).setMaxItems(maxItems);
+            int newCount = adapter.getItemCount();
+
+            if (newCount == originalCount) {
+                return;
+            }
+
+            if (newCount < originalCount) {
+                adapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
+            } else {
+                adapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
             }
         }
     }
-
-    static class SavedState extends BaseSavedState {
-        SparseArray<Parcelable> mNestedRecyclerViewState;
-
-        SavedState(Parcelable superState) {
-            super(superState);
-            mNestedRecyclerViewState = new SparseArray<>();
-        }
-
-        private SavedState(Parcel in, ClassLoader classLoader) {
-            super(in, classLoader);
-            mNestedRecyclerViewState = in.readSparseArray(classLoader);
-        }
-
-        @SuppressWarnings("unchecked")
-        @Override
-        public void writeToParcel(Parcel out, int flags) {
-            super.writeToParcel(out, flags);
-            out.writeSparseArray((SparseArray<Object>) (Object) mNestedRecyclerViewState);
-        }
-
-        public static final Parcelable.Creator<SavedState> CREATOR =
-                new Parcelable.Creator<SavedState>() {
-                    @Override
-                    public SavedState createFromParcel(Parcel in) {
-                        return new SavedState(in, getClass().getClassLoader());
-                    }
-
-                    @Override
-                    public SavedState[] newArray(int size) {
-                        return new SavedState[size];
-                    }
-                };
-    }
 }
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapter.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapter.java
index c5cf3e2..5b74b44 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapter.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapter.java
@@ -15,6 +15,8 @@
  */
 package com.android.car.ui.recyclerview;
 
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -55,7 +57,7 @@
 
         NestedRowViewHolder(View view) {
             super(view);
-            frameLayout = view.findViewById(R.id.nested_recycler_view_layout);
+            frameLayout = requireViewByRefId(view, R.id.nested_recycler_view_layout);
         }
     }
 }
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
index 2df4d95..8ced67a 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
@@ -26,6 +26,7 @@
 
 import com.android.car.ui.R;
 import com.android.car.ui.utils.CarUiUtils;
+
 /**
  * Code drop from {androidx.car.widget.CarUiSmoothScroller}
  *
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSnapHelper.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSnapHelper.java
index f98b8f7..807d856 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSnapHelper.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSnapHelper.java
@@ -35,14 +35,26 @@
  * RecyclerView is scrolling horizontally.
  */
 public class CarUiSnapHelper extends LinearSnapHelper {
+    /**
+     * The percentage of a View that needs to be completely visible for it to be a viable snap
+     * target.
+     */
+    private static final float VIEW_VISIBLE_THRESHOLD = 0.5f;
+
+    /**
+     * When a View is longer than containing RecyclerView, the percentage of the end of this View
+     * that needs to be completely visible to prevent the rest of views to be a viable snap target.
+     *
+     * <p>In other words, if a longer-than-screen View takes more than threshold screen space on its
+     * end, do not snap to any View.
+     */
+    private static final float LONG_ITEM_END_VISIBLE_THRESHOLD = 0.3f;
 
     private final Context mContext;
     private RecyclerView mRecyclerView;
-    private RecyclerView.SmoothScroller mSmoothScroller;
 
     public CarUiSnapHelper(Context context) {
-        this.mContext = context;
-        mSmoothScroller = new CarUiSmoothScroller(mContext);
+        mContext = context;
     }
 
     // Orientation helpers are lazily created per LayoutManager.
@@ -57,15 +69,12 @@
         int[] out = new int[2];
         if (layoutManager.canScrollHorizontally()) {
             out[0] = distanceToTopMargin(targetView, getHorizontalHelper(layoutManager));
-        } else {
-            out[0] = 0;
         }
 
         if (layoutManager.canScrollVertically()) {
             out[1] = distanceToTopMargin(targetView, getVerticalHelper(layoutManager));
-        } else {
-            out[1] = 0;
         }
+
         return out;
     }
 
@@ -73,20 +82,32 @@
      * Smooth scrolls the RecyclerView by a given distance.
      */
     public void smoothScrollBy(int scrollDistance) {
-        int position = findTargetSnapPosition(mRecyclerView.getLayoutManager(), scrollDistance);
+        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+        if (layoutManager == null) {
+            return;
+        }
+
+        int position = findTargetSnapPosition(layoutManager, scrollDistance);
         if (position == RecyclerView.NO_POSITION) {
             mRecyclerView.smoothScrollBy(0, scrollDistance);
             return;
         }
-        mSmoothScroller.setTargetPosition(position);
-        mRecyclerView.getLayoutManager().startSmoothScroll(mSmoothScroller);
+
+        RecyclerView.SmoothScroller scroller = createScroller(layoutManager);
+
+        if (scroller == null) {
+            return;
+        }
+
+        scroller.setTargetPosition(position);
+        layoutManager.startSmoothScroll(scroller);
     }
 
     /**
      * Finds the target position for snapping.
      *
      * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
-     * {@link RecyclerView}
+     *                      {@link RecyclerView}
      */
     private int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager,
             int scrollDistance) {
@@ -157,19 +178,99 @@
         return targetPos;
     }
 
-
+    /**
+     * Finds the view to snap to. The view to snap to is the child of the LayoutManager that is
+     * closest to the start of the RecyclerView. The "start" depends on if the LayoutManager
+     * is scrolling horizontally or vertically. If it is horizontally scrolling, then the
+     * start is the view on the left (right if RTL). Otherwise, it is the top-most view.
+     *
+     * @param layoutManager The current {@link LayoutManager} for the attached RecyclerView.
+     * @return The View closest to the start of the RecyclerView. Returns {@code null}when:
+     * <ul>
+     *     <li>there is no item; or
+     *     <li>no visible item can fully fit in the containing RecyclerView; or
+     *     <li>an item longer than containing RecyclerView is about to scroll out.
+     * </ul>
+     */
     @Override
+    @Nullable
     public View findSnapView(LayoutManager layoutManager) {
-        OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
-
-        if (mRecyclerView.computeVerticalScrollRange() - mRecyclerView.computeVerticalScrollOffset()
-                <= orientationHelper.getTotalSpace()
-                + mRecyclerView.getPaddingTop()
-                + mRecyclerView.getPaddingBottom()) {
+        int childCount = layoutManager.getChildCount();
+        if (childCount == 0) {
             return null;
         }
 
-        return findViewIfScrollable(layoutManager);
+        OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
+
+        // If there's only one child, then that will be the snap target.
+        if (childCount == 1) {
+            View firstChild = layoutManager.getChildAt(0);
+            return isValidSnapView(firstChild, orientationHelper) ? firstChild : null;
+        }
+
+        if (mRecyclerView == null) {
+            return null;
+        }
+
+        // If the top child view is longer than the RecyclerView (long item), and it's not yet
+        // scrolled out - meaning the screen it takes up is more than threshold,
+        // do not snap to any view.
+        // This way avoids next View snapping to top "pushes" out the end of a long item.
+        View firstChild = mRecyclerView.getChildAt(0);
+        if (firstChild.getHeight() > mRecyclerView.getHeight()
+                // Long item start is scrolled past screen;
+                && orientationHelper.getDecoratedStart(firstChild) < 0
+                // and it takes up more than threshold screen size.
+                && orientationHelper.getDecoratedEnd(firstChild) > (
+                mRecyclerView.getHeight() * LONG_ITEM_END_VISIBLE_THRESHOLD)) {
+            return null;
+        }
+
+        View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
+
+        // Check if the last child visible is the last item in the list.
+        boolean lastItemVisible =
+                layoutManager.getPosition(lastVisibleChild) == layoutManager.getItemCount() - 1;
+
+        // If it is, then check how much of that view is visible.
+        float lastItemPercentageVisible = lastItemVisible
+                ? getPercentageVisible(lastVisibleChild, orientationHelper) : 0;
+
+        View closestChild = null;
+        int closestDistanceToStart = Integer.MAX_VALUE;
+        float closestPercentageVisible = 0.f;
+
+        // Iterate to find the child closest to the top and more than half way visible.
+        for (int i = 0; i < childCount; i++) {
+            View child = layoutManager.getChildAt(i);
+            int startOffset = orientationHelper.getDecoratedStart(child);
+
+            if (Math.abs(startOffset) < closestDistanceToStart) {
+                float percentageVisible = getPercentageVisible(child, orientationHelper);
+
+                if (percentageVisible > VIEW_VISIBLE_THRESHOLD
+                        && percentageVisible > closestPercentageVisible) {
+                    closestDistanceToStart = startOffset;
+                    closestChild = child;
+                    closestPercentageVisible = percentageVisible;
+                }
+            }
+        }
+
+        View childToReturn = closestChild;
+
+        // If closestChild is null, then that means we were unable to find a closest child that
+        // is over the VIEW_VISIBLE_THRESHOLD. This could happen if the views are larger than
+        // the given area. In this case, consider returning the lastVisibleChild so that the screen
+        // scrolls. Also, check if the last item should be displayed anyway if it is mostly visible.
+        if ((childToReturn == null
+                || (lastItemVisible && lastItemPercentageVisible > closestPercentageVisible))) {
+            childToReturn = lastVisibleChild;
+        }
+
+        // Return null if the childToReturn is not valid. This allows the user to scroll freely
+        // with no snapping. This can allow them to see the entire view.
+        return isValidSnapView(childToReturn, orientationHelper) ? childToReturn : null;
     }
 
     private View findViewIfScrollable(LayoutManager layoutManager) {
@@ -194,7 +295,7 @@
      * view on the left (right if RTL). Otherwise, it is the top-most view.
      *
      * @param layoutManager The current {@link RecyclerView.LayoutManager} for the attached
-     * RecyclerView.
+     *                      RecyclerView.
      * @return The View closest to the start of the RecyclerView.
      */
     private static View findTopView(LayoutManager layoutManager, OrientationHelper helper) {
@@ -223,67 +324,80 @@
     }
 
     /**
+     * Returns whether or not the given View is a valid snapping view. A view is considered valid
+     * for snapping if it can fit entirely within the height of the RecyclerView it is contained
+     * within.
+     *
+     * <p>If the view is larger than the RecyclerView, then it might not want to be snapped to
+     * to allow the user to scroll and see the rest of the View.
+     *
+     * @param view   The view to determine the snapping potential.
+     * @param helper The {@link OrientationHelper} associated with the current RecyclerView.
+     * @return {@code true} if the given view is a valid snapping view; {@code false} otherwise.
+     */
+    private boolean isValidSnapView(View view, OrientationHelper helper) {
+        return helper.getDecoratedMeasurement(view) <= helper.getTotalSpace();
+    }
+
+    /**
      * Returns the percentage of the given view that is visible, relative to its containing
      * RecyclerView.
      *
-     * @param view The View to get the percentage visible of.
+     * @param view   The View to get the percentage visible of.
      * @param helper An {@link OrientationHelper} to aid with calculation.
      * @return A float indicating the percentage of the given view that is visible.
      */
-    private static float getPercentageVisible(View view, OrientationHelper helper) {
-
+    private float getPercentageVisible(View view, OrientationHelper helper) {
         int start = helper.getStartAfterPadding();
         int end = helper.getEndAfterPadding();
 
-        int viewHeight = helper.getDecoratedMeasurement(view);
-
         int viewStart = helper.getDecoratedStart(view);
         int viewEnd = helper.getDecoratedEnd(view);
 
-        if (viewEnd < start) {
-            // The is outside of the bounds of the recyclerView.
-            return 0f;
-        } else if (viewStart >= start && viewEnd <= end) {
+        if (viewStart >= start && viewEnd <= end) {
             // The view is within the bounds of the RecyclerView, so it's fully visible.
             return 1.f;
+        } else if (viewEnd <= start) {
+            // The view is above the visible area of the RecyclerView.
+            return 0;
+        } else if (viewStart >= end) {
+            // The view is below the visible area of the RecyclerView.
+            return 0;
         } else if (viewStart <= start && viewEnd >= end) {
             // The view is larger than the height of the RecyclerView.
-            return 1.f - ((float) (Math.abs(viewStart) + Math.abs(viewEnd)) / viewHeight);
+            return ((float) end - start) / helper.getDecoratedMeasurement(view);
         } else if (viewStart < start) {
-            // The view is above the start of the RecyclerView, so subtract the start offset
-            // from the total height.
-            return 1.f - ((float) Math.abs(viewStart) / helper.getDecoratedMeasurement(view));
+            // The view is above the start of the RecyclerView.
+            return ((float) viewEnd - start) / helper.getDecoratedMeasurement(view);
         } else {
-            // The view is below the end of the RecyclerView, so subtract the end offset from the
-            // total height.
-            return 1.f - ((float) Math.abs(viewEnd) / helper.getDecoratedMeasurement(view));
+            // The view is below the end of the RecyclerView.
+            return ((float) end - viewStart) / helper.getDecoratedMeasurement(view);
         }
     }
 
     @Override
     public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
-        this.mRecyclerView = recyclerView;
         super.attachToRecyclerView(recyclerView);
+        mRecyclerView = recyclerView;
     }
 
     /**
-     * Returns a scroller specific to this {@code CarUiSnapHelper}. This scroller is used for all
+     * Returns a scroller specific to this {@code PagedSnapHelper}. This scroller is used for all
      * smooth scrolling operations, including flings.
      *
-     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
-     * {@link
-     * RecyclerView}.
+     * @param layoutManager The {@link LayoutManager} associated with the attached
+     *                      {@link RecyclerView}.
      * @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling.
      */
     @Override
-    protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
-        return mSmoothScroller;
+    protected RecyclerView.SmoothScroller createScroller(@NonNull LayoutManager layoutManager) {
+        return new CarUiSmoothScroller(mContext);
     }
 
     /**
-     * Calculate the estimated scroll distance in each direction given velocities on both axes. This
-     * method will clamp the maximum scroll distance so that a single fling will never scroll more
-     * than one page.
+     * Calculate the estimated scroll distance in each direction given velocities on both axes.
+     * This method will clamp the maximum scroll distance so that a single fling will never scroll
+     * more than one page.
      *
      * @param velocityX Fling velocity on the horizontal axis.
      * @param velocityY Fling velocity on the vertical axis.
@@ -297,7 +411,7 @@
             return outDist;
         }
 
-        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
         if (layoutManager == null || layoutManager.getChildCount() == 0) {
             return outDist;
         }
@@ -325,34 +439,36 @@
         return outDist;
     }
 
-    /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
-    boolean isAtStart(RecyclerView.LayoutManager layoutManager) {
+    /**
+     * Returns {@code true} if the RecyclerView is completely displaying the first item.
+     */
+    public boolean isAtStart(@Nullable LayoutManager layoutManager) {
         if (layoutManager == null || layoutManager.getChildCount() == 0) {
             return true;
         }
 
         View firstChild = layoutManager.getChildAt(0);
         OrientationHelper orientationHelper =
-                layoutManager.canScrollVertically()
-                        ? getVerticalHelper(layoutManager)
+                layoutManager.canScrollVertically() ? getVerticalHelper(layoutManager)
                         : getHorizontalHelper(layoutManager);
 
         // Check that the first child is completely visible and is the first item in the list.
         return orientationHelper.getDecoratedStart(firstChild)
-                >= orientationHelper.getStartAfterPadding()
-                && layoutManager.getPosition(firstChild) == 0;
+                >= orientationHelper.getStartAfterPadding() && layoutManager.getPosition(firstChild)
+                == 0;
     }
 
-    /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
-    public boolean isAtEnd(RecyclerView.LayoutManager layoutManager) {
+    /**
+     * Returns {@code true} if the RecyclerView is completely displaying the last item.
+     */
+    public boolean isAtEnd(@Nullable LayoutManager layoutManager) {
         if (layoutManager == null || layoutManager.getChildCount() == 0) {
             return true;
         }
 
         int childCount = layoutManager.getChildCount();
         OrientationHelper orientationHelper =
-                layoutManager.canScrollVertically()
-                        ? getVerticalHelper(layoutManager)
+                layoutManager.canScrollVertically() ? getVerticalHelper(layoutManager)
                         : getHorizontalHelper(layoutManager);
 
         View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
@@ -366,18 +482,17 @@
 
     /**
      * Returns an {@link OrientationHelper} that corresponds to the current scroll direction of the
-     * given {@link RecyclerView.LayoutManager}.
+     * given {@link LayoutManager}.
      */
     @NonNull
-    private OrientationHelper getOrientationHelper(
-            @NonNull RecyclerView.LayoutManager layoutManager) {
+    private OrientationHelper getOrientationHelper(@NonNull LayoutManager layoutManager) {
         return layoutManager.canScrollVertically()
                 ? getVerticalHelper(layoutManager)
                 : getHorizontalHelper(layoutManager);
     }
 
     @NonNull
-    private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
+    private OrientationHelper getVerticalHelper(@NonNull LayoutManager layoutManager) {
         if (mVerticalHelper == null || mVerticalHelper.getLayoutManager() != layoutManager) {
             mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
         }
@@ -385,8 +500,7 @@
     }
 
     @NonNull
-    private OrientationHelper getHorizontalHelper(
-            @NonNull RecyclerView.LayoutManager layoutManager) {
+    private OrientationHelper getHorizontalHelper(@NonNull LayoutManager layoutManager) {
         if (mHorizontalHelper == null || mHorizontalHelper.getLayoutManager() != layoutManager) {
             mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
         }
@@ -399,8 +513,8 @@
      * parameters are not well-formed, this method's behavior is undefined.
      *
      * @param value The value to clamp.
-     * @param min The minimum value the given value can be.
-     * @param max The maximum value the given value can be.
+     * @param min   The minimum value the given value can be.
+     * @param max   The maximum value the given value can be.
      * @return A number that falls between {@code min} or {@code max} or one of those values if the
      * given value is less than or greater than {@code min} and {@code max} respectively.
      */
@@ -409,7 +523,8 @@
     }
 
     private static int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
-            OrientationHelper helper, int scrollDistance) {
+            OrientationHelper helper,
+            int scrollDistance) {
         int[] distances = new int[]{scrollDistance, scrollDistance};
         float distancePerChild = computeDistancePerChild(layoutManager, helper);
 
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java b/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
index 263e681..7b38504 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
@@ -15,17 +15,14 @@
  */
 package com.android.car.ui.recyclerview;
 
-import android.content.Context;
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
 import android.content.res.Resources;
 import android.os.Handler;
-import android.view.Gravity;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.Interpolator;
-import android.widget.FrameLayout;
 import android.widget.ImageView;
 
 import androidx.annotation.IntRange;
@@ -34,7 +31,6 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.ui.R;
-import com.android.car.ui.recyclerview.CarUiRecyclerView.ScrollBarPosition;
 import com.android.car.ui.utils.CarUiUtils;
 
 /**
@@ -73,52 +69,31 @@
     private OrientationHelper mOrientationHelper;
 
     @Override
-    public void initialize(
-            RecyclerView rv,
-            int scrollBarContainerWidth,
-            @ScrollBarPosition int scrollBarPosition,
-            boolean scrollBarAboveRecyclerView) {
-
+    public void initialize(RecyclerView rv, View scrollView) {
         mRecyclerView = rv;
 
-        LayoutInflater inflater =
-                (LayoutInflater) rv.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
-        FrameLayout parent = (FrameLayout) getRecyclerView().getParent();
-
-        mScrollView = inflater.inflate(R.layout.car_ui_recyclerview_scrollbar, parent, false);
-        mScrollView.setLayoutParams(
-                new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+        mScrollView = scrollView;
 
         Resources res = rv.getContext().getResources();
 
         mButtonDisabledAlpha = CarUiUtils.getFloat(res, R.dimen.car_ui_button_disabled_alpha);
 
-        if (scrollBarAboveRecyclerView) {
-            parent.addView(mScrollView);
-        } else {
-            parent.addView(mScrollView, /* index= */ 0);
-        }
-
-        setScrollBarContainerWidth(scrollBarContainerWidth);
-        setScrollBarPosition(scrollBarPosition);
-
         getRecyclerView().addOnScrollListener(mRecyclerViewOnScrollListener);
         getRecyclerView().getRecycledViewPool().setMaxRecycledViews(0, 12);
 
         mSeparatingMargin = res.getDimensionPixelSize(R.dimen.car_ui_scrollbar_separator_margin);
 
-        mUpButton = mScrollView.findViewById(R.id.page_up);
+        mUpButton = requireViewByRefId(mScrollView, R.id.page_up);
         PaginateButtonClickListener upButtonClickListener =
                 new PaginateButtonClickListener(PaginationListener.PAGE_UP);
         mUpButton.setOnClickListener(upButtonClickListener);
 
-        mDownButton = mScrollView.findViewById(R.id.page_down);
+        mDownButton = requireViewByRefId(mScrollView, R.id.page_down);
         PaginateButtonClickListener downButtonClickListener =
                 new PaginateButtonClickListener(PaginationListener.PAGE_DOWN);
         mDownButton.setOnClickListener(downButtonClickListener);
 
-        mScrollThumb = mScrollView.findViewById(R.id.scrollbar_thumb);
+        mScrollThumb = requireViewByRefId(mScrollView, R.id.scrollbar_thumb);
 
         mSnapHelper = new CarUiSnapHelper(rv.getContext());
         getRecyclerView().setOnFlingListener(null);
@@ -168,19 +143,6 @@
         mScrollView.requestLayout();
     }
 
-    /**
-     * Sets the width of the container that holds the scrollbar. The scrollbar will be centered
-     * within
-     * this width.
-     *
-     * @param width The width of the scrollbar container.
-     */
-    private void setScrollBarContainerWidth(int width) {
-        ViewGroup.LayoutParams layoutParams = mScrollView.getLayoutParams();
-        layoutParams.width = width;
-        mScrollView.requestLayout();
-    }
-
     @Override
     public void setPadding(int paddingStart, int paddingEnd) {
         this.mPaddingStart = paddingStart;
@@ -189,23 +151,6 @@
     }
 
     /**
-     * Sets the position of the scrollbar.
-     *
-     * @param position Enum value of the scrollbar position. 0 for Start and 1 for end.
-     */
-    private void setScrollBarPosition(@ScrollBarPosition int position) {
-        FrameLayout.LayoutParams layoutParams =
-                (FrameLayout.LayoutParams) mScrollView.getLayoutParams();
-        if (position == ScrollBarPosition.START) {
-            layoutParams.gravity = Gravity.LEFT;
-        } else {
-            layoutParams.gravity = Gravity.RIGHT;
-        }
-
-        mScrollView.requestLayout();
-    }
-
-    /**
      * Sets whether or not the up button on the scroll bar is clickable.
      *
      * @param enabled {@code true} if the up button is enabled.
@@ -446,7 +391,6 @@
         OrientationHelper orientationHelper =
                 getOrientationHelper(getRecyclerView().getLayoutManager());
         int screenSize = orientationHelper.getTotalSpace();
-
         int scrollDistance = screenSize;
         // The iteration order matters. In case where there are 2 items longer than screen size, we
         // want to focus on upcoming view.
@@ -472,7 +416,7 @@
                 break;
             }
         }
-
+        // Distance should always be positive. Negate its value to scroll up.
         mSnapHelper.smoothScrollBy(-scrollDistance);
     }
 
@@ -497,16 +441,14 @@
 
         // If the last item is partially visible, page down should bring it to the top.
         View lastChild = getRecyclerView().getChildAt(getRecyclerView().getChildCount() - 1);
-        if (getRecyclerView()
-                .getLayoutManager()
-                .isViewPartiallyVisible(
-                        lastChild, /* completelyVisible= */ false, /* acceptEndPointInclusion= */
-                        false)) {
+        if (getRecyclerView().getLayoutManager().isViewPartiallyVisible(lastChild,
+                /* completelyVisible= */ false, /* acceptEndPointInclusion= */ false)) {
             scrollDistance = orientationHelper.getDecoratedStart(lastChild);
-            if (scrollDistance < 0) {
-                // Scroll value can be negative if the child is longer than the screen size and the
-                // visible area of the screen does not show the start of the child.
-                // Scroll to the next screen if the start value is negative
+            if (scrollDistance <= 0) {
+                // - Scroll value is zero if the top of last item is aligned with top of the screen;
+                // - Scroll value can be negative if the child is longer than the screen size and
+                //   the visible area of the screen does not show the start of the child.
+                // Scroll to the next screen in both cases.
                 scrollDistance = screenSize;
             }
         }
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/ScrollBar.java b/car-ui-lib/src/com/android/car/ui/recyclerview/ScrollBar.java
index 5db592a..ce58897 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/ScrollBar.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/ScrollBar.java
@@ -15,9 +15,9 @@
  */
 package com.android.car.ui.recyclerview;
 
-import androidx.recyclerview.widget.RecyclerView;
+import android.view.View;
 
-import com.android.car.ui.recyclerview.CarUiRecyclerView.ScrollBarPosition;
+import androidx.recyclerview.widget.RecyclerView;
 
 /**
  * An abstract class that defines required contract for a custom scroll bar for the {@link
@@ -28,11 +28,7 @@
      * The concrete class should implement this method to initialize configuration of a scrollbar
      * view.
      */
-    void initialize(
-            RecyclerView recyclerView,
-            int scrollBarContainerWidth,
-            @ScrollBarPosition int scrollBarPosition,
-            boolean scrollBarAboveRecyclerView);
+    void initialize(RecyclerView recyclerView, View scrollView);
 
     /**
      * Requests layout of the scrollbar. Should be called when there's been a change that will
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
index ef142ec..20d6847 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
@@ -309,8 +309,8 @@
     public static final class Builder {
         private final Context mContext;
 
-        private CharSequence mSearchTitle;
-        private CharSequence mSettingsTitle;
+        private String mSearchTitle;
+        private String mSettingsTitle;
         private Drawable mSearchIcon;
         private Drawable mSettingsIcon;
 
@@ -353,7 +353,7 @@
                 throw new IllegalStateException("Can't have both a search and settings MenuItem");
             }
 
-            if (mIsSearch && (!mSearchTitle.equals(mTitle)
+            if (mIsSearch && (!mSearchTitle.contentEquals(mTitle)
                     || !mSearchIcon.equals(mIcon)
                     || mIsCheckable
                     || mIsActivatable
@@ -363,14 +363,13 @@
                 throw new IllegalStateException("Invalid search MenuItem");
             }
 
-            if (mIsSettings && (!mSettingsTitle.equals(mTitle)
+            if (mIsSettings && (!mSettingsTitle.contentEquals(mTitle)
                     || !mSettingsIcon.equals(mIcon)
                     || mIsCheckable
                     || mIsActivatable
                     || !mIsTinted
                     || mShowIconAndTitle
-                    || mDisplayBehavior != DisplayBehavior.ALWAYS
-                    || mUxRestrictions != CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP)) {
+                    || mDisplayBehavior != DisplayBehavior.ALWAYS)) {
                 throw new IllegalStateException("Invalid settings MenuItem");
             }
 
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
index b6ba3fc..77b9f53 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
@@ -15,6 +15,8 @@
  */
 package com.android.car.ui.toolbar;
 
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
 import android.app.Activity;
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
@@ -32,6 +34,7 @@
 
 import androidx.annotation.XmlRes;
 import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
+import androidx.core.util.Consumer;
 
 import com.android.car.ui.R;
 import com.android.car.ui.utils.CarUiUtils;
@@ -45,7 +48,6 @@
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.CompletableFuture;
 
 class MenuItemRenderer implements MenuItem.Listener {
 
@@ -90,22 +92,22 @@
         updateView();
     }
 
-    CompletableFuture<View> createView() {
-        CompletableFuture<View> future = new CompletableFuture<>();
+    void createView(Consumer<View> callback) {
         AsyncLayoutInflater inflater = new AsyncLayoutInflater(mParentView.getContext());
         inflater.inflate(R.layout.car_ui_toolbar_menu_item, mParentView, (View view, int resid,
                 ViewGroup parent) -> {
             mView = view;
-            mIconContainer = mView.requireViewById(R.id.car_ui_toolbar_menu_item_icon_container);
-            mIconView = mView.requireViewById(R.id.car_ui_toolbar_menu_item_icon);
-            mSwitch = mView.requireViewById(R.id.car_ui_toolbar_menu_item_switch);
-            mTextView = mView.requireViewById(R.id.car_ui_toolbar_menu_item_text);
-            mTextWithIconView = mView.requireViewById(R.id.car_ui_toolbar_menu_item_text_with_icon);
-            updateView();
-            future.complete(view);
-        });
 
-        return future;
+            mIconContainer =
+                    requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_icon_container);
+            mIconView = requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_icon);
+            mSwitch = requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_switch);
+            mTextView = requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_text);
+            mTextWithIconView =
+                    requireViewByRefId(mView, R.id.car_ui_toolbar_menu_item_text_with_icon);
+            updateView();
+            callback.accept(mView);
+        });
     }
 
     private void updateView() {
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/SearchView.java b/car-ui-lib/src/com/android/car/ui/toolbar/SearchView.java
index d7240eb..7f7eb80 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/SearchView.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/SearchView.java
@@ -15,6 +15,8 @@
  */
 package com.android.car.ui.toolbar;
 
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.text.Editable;
@@ -28,6 +30,7 @@
 import android.widget.EditText;
 import android.widget.ImageView;
 
+import androidx.annotation.NonNull;
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.car.ui.R;
@@ -83,9 +86,10 @@
         LayoutInflater inflater = LayoutInflater.from(context);
         inflater.inflate(R.layout.car_ui_toolbar_search_view, this, true);
 
-        mSearchText = requireViewById(R.id.car_ui_toolbar_search_bar);
-        mIcon = requireViewById(R.id.car_ui_toolbar_search_icon);
-        mCloseIcon = requireViewById(R.id.car_ui_toolbar_search_close);
+        mSearchText = requireViewByRefId(this, R.id.car_ui_toolbar_search_bar);
+        mIcon = requireViewByRefId(this, R.id.car_ui_toolbar_search_icon);
+        mCloseIcon = requireViewByRefId(this, R.id.car_ui_toolbar_search_close);
+
         mCloseIcon.setOnClickListener(view -> mSearchText.getText().clear());
         mCloseIcon.setVisibility(View.GONE);
 
@@ -95,6 +99,7 @@
         mEndPadding = context.getResources().getDimensionPixelSize(
                 R.dimen.car_ui_toolbar_search_close_icon_container_width);
 
+        mSearchText.setSaveEnabled(false);
         mSearchText.setPaddingRelative(mStartPadding, 0, mEndPadding, 0);
 
         mSearchText.setOnFocusChangeListener(
@@ -120,20 +125,19 @@
         });
     }
 
+    private boolean mWasShown = false;
+
     @Override
-    public void setVisibility(int visibility) {
-        boolean showing = visibility == View.VISIBLE && getVisibility() != View.VISIBLE;
+    public void onVisibilityChanged(@NonNull View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
 
-        super.setVisibility(visibility);
-
-        if (showing) {
-            mSearchText.removeTextChangedListener(mTextWatcher);
-            mSearchText.getText().clear();
-            mSearchText.addTextChangedListener(mTextWatcher);
-            mCloseIcon.setVisibility(View.GONE);
-
+        boolean isShown = isShown();
+        if (isShown && !mWasShown) {
+            boolean hasQuery = mSearchText.getText().length() > 0;
+            mCloseIcon.setVisibility(hasQuery ? View.VISIBLE : View.GONE);
             mSearchText.requestFocus();
         }
+        mWasShown = isShown;
     }
 
     /**
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/TabLayout.java b/car-ui-lib/src/com/android/car/ui/toolbar/TabLayout.java
index b30c370..828e54a 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/TabLayout.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/TabLayout.java
@@ -15,6 +15,8 @@
  */
 package com.android.car.ui.toolbar;
 
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -77,9 +79,6 @@
         }
     }
 
-    // View attributes
-    private final boolean mTabFlexibleLayout;
-
     private final Set<Listener> mListeners = new ArraySet<>();
 
     private final TabAdapter mTabAdapter;
@@ -96,9 +95,11 @@
         super(context, attrs, defStyle);
         Resources resources = context.getResources();
 
-        mTabFlexibleLayout = resources.getBoolean(R.bool.car_ui_toolbar_tab_flexible_layout);
-
-        mTabAdapter = new TabAdapter(context, R.layout.car_ui_toolbar_tab_item_layout, this);
+        boolean tabFlexibleLayout = resources.getBoolean(R.bool.car_ui_toolbar_tab_flexible_layout);
+        @LayoutRes int tabLayoutRes = tabFlexibleLayout
+                ? R.layout.car_ui_toolbar_tab_item_layout_flexible
+                : R.layout.car_ui_toolbar_tab_item_layout;
+        mTabAdapter = new TabAdapter(context, tabLayoutRes, this);
     }
 
     /**
@@ -172,15 +173,7 @@
     }
 
     private void addTabView(View tabView, int position) {
-        LayoutParams layoutParams;
-        if (mTabFlexibleLayout) {
-            layoutParams = new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT);
-            layoutParams.weight = 1;
-        } else {
-            layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
-                    ViewGroup.LayoutParams.MATCH_PARENT);
-        }
-        addView(tabView, position, layoutParams);
+        addView(tabView, position);
     }
 
     private static class TabAdapter extends BaseAdapter {
@@ -282,8 +275,8 @@
         private void presentTabItemView(int position, @NonNull View tabItemView) {
             Tab tab = mTabList.get(position);
 
-            ImageView iconView = tabItemView.findViewById(R.id.car_ui_toolbar_tab_item_icon);
-            TextView textView = tabItemView.findViewById(R.id.car_ui_toolbar_tab_item_text);
+            ImageView iconView = requireViewByRefId(tabItemView, R.id.car_ui_toolbar_tab_item_icon);
+            TextView textView = requireViewByRefId(tabItemView, R.id.car_ui_toolbar_tab_item_text);
 
             tabItemView.setOnClickListener(view -> selectTab(tab));
             tab.bindText(textView);
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
index 6069e5f..697c783 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
@@ -15,8 +15,6 @@
  */
 package com.android.car.ui.toolbar;
 
-import android.app.Activity;
-import android.app.AlertDialog;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
@@ -24,13 +22,8 @@
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
 import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
 import android.widget.ProgressBar;
-import android.widget.TextView;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
@@ -39,16 +32,8 @@
 import androidx.annotation.XmlRes;
 
 import com.android.car.ui.R;
-import com.android.car.ui.utils.CarUiUtils;
-import com.android.car.ui.utils.CarUxRestrictionsUtil;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
 
 /**
  * A toolbar for Android Automotive OS apps.
@@ -59,7 +44,7 @@
  *
  * <p>The toolbar supports a navigation button, title, tabs, search, and {@link MenuItem MenuItems}
  */
-public class Toolbar extends FrameLayout {
+public class Toolbar extends FrameLayout implements ToolbarController {
 
     /** Callback that will be issued whenever the height of toolbar is changed. */
     public interface OnHeightChangedListener {
@@ -133,52 +118,9 @@
         EDIT,
     }
 
-    private final boolean mIsTabsInSecondRow;
-
-    private ImageView mNavIcon;
-    private ImageView mLogoInNavIconSpace;
-    private ViewGroup mNavIconContainer;
-    private TextView mTitle;
-    private ImageView mTitleLogo;
-    private ViewGroup mTitleLogoContainer;
-    private TabLayout mTabLayout;
-    private LinearLayout mMenuItemsContainer;
-    private FrameLayout mSearchViewContainer;
-    private SearchView mSearchView;
-
-    // Cached values that we will send to views when they are inflated
-    private CharSequence mSearchHint;
-    private Drawable mSearchIcon;
-    private String mSearchQuery;
-    private final Set<OnSearchListener> mOnSearchListeners = new HashSet<>();
-    private final Set<OnSearchCompletedListener> mOnSearchCompletedListeners = new HashSet<>();
-
-    private final Set<OnBackListener> mOnBackListeners = new HashSet<>();
-    private final Set<OnTabSelectedListener> mOnTabSelectedListeners = new HashSet<>();
-    private final Set<OnHeightChangedListener> mOnHeightChangedListeners = new HashSet<>();
-
-    private final MenuItem mOverflowButton;
-    private boolean mHasLogo = false;
-    private boolean mShowMenuItemsWhileSearching;
-    private State mState = State.HOME;
-    private NavButtonMode mNavButtonMode = NavButtonMode.BACK;
-    @NonNull
-    private List<MenuItem> mMenuItems = Collections.emptyList();
-    private List<MenuItem> mOverflowItems = new ArrayList<>();
-    private final List<MenuItemRenderer> mMenuItemRenderers = new ArrayList<>();
-    private CompletableFuture<Void> mMenuItemViewsFuture;
-    private int mMenuItemsXmlId = 0;
-    private AlertDialog mOverflowDialog;
-    private boolean mNavIconSpaceReserved;
-    private boolean mLogoFillsNavIconSpace;
-    private boolean mShowLogo;
+    private ToolbarControllerImpl mController;
     private boolean mEatingTouch = false;
     private boolean mEatingHover = false;
-    private ProgressBar mProgressBar;
-    private MenuItem.Listener mOverflowItemListener = () -> {
-        createOverflowDialog();
-        setState(getState());
-    };
 
     public Toolbar(Context context) {
         this(context, null);
@@ -195,55 +137,21 @@
     public Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
 
-        mOverflowButton = MenuItem.builder(getContext())
-                .setIcon(R.drawable.car_ui_icon_overflow_menu)
-                .setTitle(R.string.car_ui_toolbar_menu_item_overflow_title)
-                .setOnClickListener(v -> {
-                    if (mOverflowDialog == null) {
-                        if (Log.isLoggable(TAG, Log.ERROR)) {
-                            Log.e(TAG, "Overflow dialog was null when trying to show it!");
-                        }
-                    } else {
-                        mOverflowDialog.show();
-                    }
-                })
-                .build();
+        LayoutInflater inflater = (LayoutInflater) context
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(getToolbarLayout(), this, true);
+
+        mController = new ToolbarControllerImpl(this);
 
         TypedArray a = context.obtainStyledAttributes(
                 attrs, R.styleable.CarUiToolbar, defStyleAttr, defStyleRes);
 
         try {
-
-            mIsTabsInSecondRow = context.getResources().getBoolean(
-                    R.bool.car_ui_toolbar_tabs_on_second_row);
-            mNavIconSpaceReserved = context.getResources().getBoolean(
-                    R.bool.car_ui_toolbar_nav_icon_reserve_space);
-            mLogoFillsNavIconSpace = context.getResources().getBoolean(
-                    R.bool.car_ui_toolbar_logo_fills_nav_icon_space);
-            mShowLogo = context.getResources().getBoolean(
-                    R.bool.car_ui_toolbar_show_logo);
-
-            LayoutInflater inflater = (LayoutInflater) context
-                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-            inflater.inflate(getToolbarLayout(), this, true);
-
-            mTabLayout = requireViewById(R.id.car_ui_toolbar_tabs);
-            mNavIcon = requireViewById(R.id.car_ui_toolbar_nav_icon);
-            mLogoInNavIconSpace = requireViewById(R.id.car_ui_toolbar_logo);
-            mNavIconContainer = requireViewById(R.id.car_ui_toolbar_nav_icon_container);
-            mMenuItemsContainer = requireViewById(R.id.car_ui_toolbar_menu_items_container);
-            mTitle = requireViewById(R.id.car_ui_toolbar_title);
-            mTitleLogoContainer = requireViewById(R.id.car_ui_toolbar_title_logo_container);
-            mTitleLogo = requireViewById(R.id.car_ui_toolbar_title_logo);
-            mSearchViewContainer = requireViewById(R.id.car_ui_toolbar_search_view_container);
-            mProgressBar = requireViewById(R.id.car_ui_toolbar_progress_bar);
-
-            mTitle.setText(a.getString(R.styleable.CarUiToolbar_title));
+            setShowTabsInSubpage(a.getBoolean(R.styleable.CarUiToolbar_showTabsInSubpage, false));
+            setTitle(a.getString(R.styleable.CarUiToolbar_title));
             setLogo(a.getResourceId(R.styleable.CarUiToolbar_logo, 0));
             setBackgroundShown(a.getBoolean(R.styleable.CarUiToolbar_showBackground, true));
             setMenuItems(a.getResourceId(R.styleable.CarUiToolbar_menuItems, 0));
-            mShowMenuItemsWhileSearching = a.getBoolean(
-                    R.styleable.CarUiToolbar_showMenuItemsWhileSearching, false);
             String searchHint = a.getString(R.styleable.CarUiToolbar_searchHint);
             if (searchHint != null) {
                 setSearchHint(searchHint);
@@ -285,15 +193,6 @@
         } finally {
             a.recycle();
         }
-
-        mTabLayout.addListener(new TabLayout.Listener() {
-            @Override
-            public void onTabSelected(TabLayout.Tab tab) {
-                for (OnTabSelectedListener listener : mOnTabSelectedListeners) {
-                    listener.onTabSelected(tab);
-                }
-            }
-        });
     }
 
     /**
@@ -302,47 +201,19 @@
      * <p>Non-system apps should not use this, as customising the layout isn't possible with RROs
      */
     protected int getToolbarLayout() {
-        if (mIsTabsInSecondRow) {
+        if (getContext().getResources().getBoolean(
+                R.bool.car_ui_toolbar_tabs_on_second_row)) {
             return R.layout.car_ui_toolbar_two_row;
         }
 
         return R.layout.car_ui_toolbar;
     }
 
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        for (OnHeightChangedListener listener : mOnHeightChangedListeners) {
-            listener.onHeightChanged(getHeight());
-        }
-    }
-
     /**
      * Returns {@code true} if a two row layout in enabled for the toolbar.
      */
     public boolean isTabsInSecondRow() {
-        return mIsTabsInSecondRow;
-    }
-
-    private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener
-            mOnUxRestrictionsChangedListener = restrictions -> {
-                for (MenuItemRenderer renderer : mMenuItemRenderers) {
-                    renderer.setCarUxRestrictions(restrictions);
-                }
-            };
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        CarUxRestrictionsUtil.getInstance(getContext())
-                .register(mOnUxRestrictionsChangedListener);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        CarUxRestrictionsUtil.getInstance(getContext())
-                .unregister(mOnUxRestrictionsChangedListener);
+        return mController.isTabsInSecondRow();
     }
 
     /**
@@ -351,8 +222,7 @@
      * <p>The title may not always be shown, for example with one row layout with tabs.
      */
     public void setTitle(@StringRes int title) {
-        mTitle.setText(title);
-        setState(getState());
+        mController.setTitle(title);
     }
 
     /**
@@ -361,19 +231,18 @@
      * <p>The title may not always be shown, for example with one row layout with tabs.
      */
     public void setTitle(CharSequence title) {
-        mTitle.setText(title);
-        setState(getState());
+        mController.setTitle(title);
     }
 
     public CharSequence getTitle() {
-        return mTitle.getText();
+        return mController.getTitle();
     }
 
     /**
      * Gets the {@link TabLayout} for this toolbar.
      */
     public TabLayout getTabLayout() {
-        return mTabLayout;
+        return mController.getTabLayout();
     }
 
     /**
@@ -381,14 +250,12 @@
      * {@link #registerOnTabSelectedListener(OnTabSelectedListener)}.
      */
     public void addTab(TabLayout.Tab tab) {
-        mTabLayout.addTab(tab);
-        setState(getState());
+        mController.addTab(tab);
     }
 
     /** Removes all the tabs. */
     public void clearAllTabs() {
-        mTabLayout.clearAllTabs();
-        setState(getState());
+        mController.clearAllTabs();
     }
 
     /**
@@ -396,7 +263,7 @@
      * {@link #addTab(TabLayout.Tab)}.
      */
     public TabLayout.Tab getTab(int position) {
-        return mTabLayout.get(position);
+        return mController.getTab(position);
     }
 
     /**
@@ -404,7 +271,21 @@
      * {@link #addTab(TabLayout.Tab)}.
      */
     public void selectTab(int position) {
-        mTabLayout.selectTab(position);
+        mController.selectTab(position);
+    }
+
+    /**
+     * Sets whether or not tabs should also be shown in the SUBPAGE {@link State}.
+     */
+    public void setShowTabsInSubpage(boolean showTabs) {
+        mController.setShowTabsInSubpage(showTabs);
+    }
+
+    /**
+     * Gets whether or not tabs should also be shown in the SUBPAGE {@link State}.
+     */
+    public boolean getShowTabsInSubpage() {
+        return mController.getShowTabsInSubpage();
     }
 
     /**
@@ -412,7 +293,7 @@
      * will be displayed next to the title.
      */
     public void setLogo(@DrawableRes int resId) {
-        setLogo(resId != 0 ? getContext().getDrawable(resId) : null);
+        mController.setLogo(resId);
     }
 
     /**
@@ -420,38 +301,22 @@
      * will be displayed next to the title.
      */
     public void setLogo(Drawable drawable) {
-        if (!mShowLogo) {
-            // If no logo should be shown then we act as if we never received one.
-            return;
-        }
-        if (drawable != null) {
-            mLogoInNavIconSpace.setImageDrawable(drawable);
-            mTitleLogo.setImageDrawable(drawable);
-            mHasLogo = true;
-        } else {
-            mHasLogo = false;
-        }
-        setState(mState);
+        mController.setLogo(drawable);
     }
 
     /** Sets the hint for the search bar. */
     public void setSearchHint(@StringRes int resId) {
-        setSearchHint(getContext().getString(resId));
+        mController.setSearchHint(resId);
     }
 
     /** Sets the hint for the search bar. */
     public void setSearchHint(CharSequence hint) {
-        if (!Objects.equals(hint, mSearchHint)) {
-            mSearchHint = hint;
-            if (mSearchView != null) {
-                mSearchView.setHint(mSearchHint);
-            }
-        }
+        mController.setSearchHint(hint);
     }
 
     /** Gets the search hint */
     public CharSequence getSearchHint() {
-        return mSearchHint;
+        return mController.getSearchHint();
     }
 
     /**
@@ -461,7 +326,7 @@
      * a similar place.
      */
     public void setSearchIcon(@DrawableRes int resId) {
-        setSearchIcon(getContext().getDrawable(resId));
+        mController.setSearchIcon(resId);
     }
 
     /**
@@ -471,12 +336,7 @@
      * a similar place.
      */
     public void setSearchIcon(Drawable d) {
-        if (!Objects.equals(d, mSearchIcon)) {
-            mSearchIcon = d;
-            if (mSearchView != null) {
-                mSearchView.setIcon(mSearchIcon);
-            }
-        }
+        mController.setSearchIcon(d);
     }
 
     /**
@@ -494,15 +354,12 @@
 
     /** Sets the {@link NavButtonMode} */
     public void setNavButtonMode(NavButtonMode style) {
-        if (style != mNavButtonMode) {
-            mNavButtonMode = style;
-            setState(mState);
-        }
+        mController.setNavButtonMode(style);
     }
 
     /** Gets the {@link NavButtonMode} */
     public NavButtonMode getNavButtonMode() {
-        return mNavButtonMode;
+        return mController.getNavButtonMode();
     }
 
     /**
@@ -517,76 +374,19 @@
 
     /** Show/hide the background. When hidden, the toolbar is completely transparent. */
     public void setBackgroundShown(boolean shown) {
-        if (shown) {
-            super.setBackground(getContext().getDrawable(R.drawable.car_ui_toolbar_background));
-        } else {
-            super.setBackground(null);
-        }
+        mController.setBackgroundShown(shown);
     }
 
     /** Returns true is the toolbar background is shown */
     public boolean getBackgroundShown() {
-        return super.getBackground() != null;
-    }
-
-    private void setMenuItemsInternal(@Nullable List<MenuItem> items) {
-        if (items == null) {
-            items = Collections.emptyList();
-        }
-
-        if (items.equals(mMenuItems)) {
-            return;
-        }
-
-        // Copy the list so that if the list is modified and setMenuItems is called again,
-        // the equals() check will fail. Note that the MenuItems are not copied here.
-        mMenuItems = new ArrayList<>(items);
-
-        mOverflowItems.clear();
-        mMenuItemRenderers.clear();
-        mMenuItemsContainer.removeAllViews();
-
-        List<CompletableFuture<View>> viewFutures = new ArrayList<>();
-        for (MenuItem item : mMenuItems) {
-            if (item.getDisplayBehavior() == MenuItem.DisplayBehavior.NEVER) {
-                mOverflowItems.add(item);
-                item.setListener(mOverflowItemListener);
-            } else {
-                MenuItemRenderer renderer = new MenuItemRenderer(item, mMenuItemsContainer);
-                mMenuItemRenderers.add(renderer);
-                viewFutures.add(renderer.createView());
-            }
-        }
-
-        if (!mOverflowItems.isEmpty()) {
-            MenuItemRenderer renderer = new MenuItemRenderer(mOverflowButton, mMenuItemsContainer);
-            mMenuItemRenderers.add(renderer);
-            viewFutures.add(renderer.createView());
-            createOverflowDialog();
-        }
-
-        if (mMenuItemViewsFuture != null) {
-            mMenuItemViewsFuture.cancel(false);
-        }
-
-        mMenuItemViewsFuture = CompletableFuture.allOf(
-            viewFutures.toArray(new CompletableFuture[0]));
-        mMenuItemViewsFuture.thenRunAsync(() -> {
-            for (CompletableFuture<View> future : viewFutures) {
-                mMenuItemsContainer.addView(future.join());
-            }
-            mMenuItemViewsFuture = null;
-        }, getContext().getMainExecutor());
-
-        setState(mState);
+        return mController.getBackgroundShown();
     }
 
     /**
      * Sets the {@link MenuItem Menuitems} to display.
      */
     public void setMenuItems(@Nullable List<MenuItem> items) {
-        mMenuItemsXmlId = 0;
-        setMenuItemsInternal(items);
+        mController.setMenuItems(items);
     }
 
     /**
@@ -619,76 +419,25 @@
      * @return The MenuItems that were loaded from XML.
      */
     public List<MenuItem> setMenuItems(@XmlRes int resId) {
-        if (mMenuItemsXmlId != 0 && mMenuItemsXmlId == resId) {
-            return mMenuItems;
-        }
-
-        mMenuItemsXmlId = resId;
-        List<MenuItem> menuItems = MenuItemRenderer.readMenuItemList(getContext(), resId);
-        setMenuItemsInternal(menuItems);
-        return menuItems;
+        return mController.setMenuItems(resId);
     }
 
     /** Gets the {@link MenuItem MenuItems} currently displayed */
     @NonNull
     public List<MenuItem> getMenuItems() {
-        return Collections.unmodifiableList(mMenuItems);
+        return mController.getMenuItems();
     }
 
     /** Gets a {@link MenuItem} by id. */
     @Nullable
     public MenuItem findMenuItemById(int id) {
-        for (MenuItem item : mMenuItems) {
-            if (item.getId() == id) {
-                return item;
-            }
-        }
-        return null;
+        return mController.findMenuItemById(id);
     }
 
     /** Gets a {@link MenuItem} by id. Will throw an exception if not found. */
     @NonNull
     public MenuItem requireMenuItemById(int id) {
-        MenuItem result = findMenuItemById(id);
-
-        if (result == null) {
-            throw new IllegalArgumentException("ID does not reference a MenuItem on this Toolbar");
-        }
-
-        return result;
-    }
-
-    private int countVisibleOverflowItems() {
-        int numVisibleItems = 0;
-        for (MenuItem item : mOverflowItems) {
-            if (item.isVisible()) {
-                numVisibleItems++;
-            }
-        }
-        return numVisibleItems;
-    }
-
-    private void createOverflowDialog() {
-        // TODO(b/140564530) Use a carui alert with a (car ui)recyclerview here
-        // TODO(b/140563930) Support enabled/disabled overflow items
-
-        CharSequence[] itemTitles = new CharSequence[countVisibleOverflowItems()];
-        int i = 0;
-        for (MenuItem item : mOverflowItems) {
-            if (item.isVisible()) {
-                itemTitles[i++] = item.getTitle();
-            }
-        }
-
-        mOverflowDialog = new AlertDialog.Builder(getContext())
-                .setItems(itemTitles, (dialog, which) -> {
-                    MenuItem item = mOverflowItems.get(which);
-                    MenuItem.OnClickListener listener = item.getOnClickListener();
-                    if (listener != null) {
-                        listener.onClick(item);
-                    }
-                })
-                .create();
+        return mController.requireMenuItemById(id);
     }
 
     /**
@@ -697,29 +446,19 @@
      * {@link MenuItem.Builder#setToSearch()} will still be hidden.
      */
     public void setShowMenuItemsWhileSearching(boolean showMenuItems) {
-        mShowMenuItemsWhileSearching = showMenuItems;
-        setState(mState);
+        mController.setShowMenuItemsWhileSearching(showMenuItems);
     }
 
     /** Returns if {@link MenuItem MenuItems} are shown while searching */
     public boolean getShowMenuItemsWhileSearching() {
-        return mShowMenuItemsWhileSearching;
+        return mController.getShowMenuItemsWhileSearching();
     }
 
     /**
      * Sets the search query.
      */
     public void setSearchQuery(String query) {
-        if (!Objects.equals(mSearchQuery, query)) {
-            mSearchQuery = query;
-            if (mSearchView != null) {
-                mSearchView.setSearchQuery(query);
-            } else {
-                for (OnSearchListener listener : mOnSearchListeners) {
-                    listener.onSearch(query);
-                }
-            }
-        }
+        mController.setSearchQuery(query);
     }
 
     /**
@@ -727,105 +466,12 @@
      * for the desired state.
      */
     public void setState(State state) {
-        mState = state;
-
-        if (mSearchView == null && (state == State.SEARCH || state == State.EDIT)) {
-            SearchView searchView = new SearchView(getContext());
-            searchView.setHint(mSearchHint);
-            searchView.setIcon(mSearchIcon);
-            searchView.setSearchListeners(mOnSearchListeners);
-            searchView.setSearchCompletedListeners(mOnSearchCompletedListeners);
-            searchView.setSearchQuery(mSearchQuery);
-            searchView.setVisibility(View.GONE);
-
-            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
-                    ViewGroup.LayoutParams.MATCH_PARENT,
-                    ViewGroup.LayoutParams.MATCH_PARENT);
-            mSearchViewContainer.addView(searchView, layoutParams);
-
-            mSearchView = searchView;
-        }
-
-        for (MenuItemRenderer renderer : mMenuItemRenderers) {
-            renderer.setToolbarState(mState);
-        }
-
-        View.OnClickListener backClickListener = (v) -> {
-            boolean absorbed = false;
-            List<OnBackListener> listenersCopy = new ArrayList<>(mOnBackListeners);
-            for (OnBackListener listener : listenersCopy) {
-                absorbed = absorbed || listener.onBack();
-            }
-
-            if (!absorbed) {
-                Activity activity = CarUiUtils.getActivity(getContext());
-                if (activity != null) {
-                    activity.onBackPressed();
-                }
-            }
-        };
-
-        switch (mNavButtonMode) {
-            case CLOSE:
-                mNavIcon.setImageResource(R.drawable.car_ui_icon_close);
-                break;
-            case DOWN:
-                mNavIcon.setImageResource(R.drawable.car_ui_icon_down);
-                break;
-            default:
-                mNavIcon.setImageResource(R.drawable.car_ui_icon_arrow_back);
-                break;
-        }
-
-        mNavIcon.setVisibility(state != State.HOME ? VISIBLE : GONE);
-
-        // Show the logo in the nav space if that's enabled, we have a logo,
-        // and we're in the Home state.
-        mLogoInNavIconSpace.setVisibility(mHasLogo
-                && state == State.HOME
-                && mLogoFillsNavIconSpace
-                ? VISIBLE : INVISIBLE);
-
-        // Show logo next to the title if we're in the subpage state or we're configured to not show
-        // the logo in the nav icon space.
-        mTitleLogoContainer.setVisibility(mHasLogo
-                && (state == State.SUBPAGE || !mLogoFillsNavIconSpace)
-                ? VISIBLE : GONE);
-
-        // Show the nav icon container if we're not in the home space or the logo fills the nav icon
-        // container. If car_ui_toolbar_nav_icon_reserve_space is true, hiding it will still reserve
-        // its space
-        mNavIconContainer.setVisibility(state != State.HOME || (mHasLogo && mLogoFillsNavIconSpace)
-                ? VISIBLE : (mNavIconSpaceReserved ? INVISIBLE : GONE));
-        mNavIconContainer.setOnClickListener(state != State.HOME ? backClickListener : null);
-        mNavIconContainer.setClickable(state != State.HOME);
-
-        boolean hasTabs = mTabLayout.getTabCount() > 0;
-        // Show the title if we're in the subpage state, or in the home state with no tabs or tabs
-        // on the second row
-        mTitle.setVisibility(state == State.SUBPAGE
-                || (state == State.HOME && (!hasTabs || mIsTabsInSecondRow))
-                ? VISIBLE : GONE);
-        mTabLayout.setVisibility(state == State.HOME && hasTabs ? VISIBLE : GONE);
-
-        if (mSearchView != null) {
-            if (state == State.SEARCH || state == State.EDIT) {
-                mSearchView.setPlainText(state == State.EDIT);
-                mSearchView.setVisibility(VISIBLE);
-            } else {
-                mSearchView.setVisibility(GONE);
-            }
-        }
-
-        boolean showButtons = (state != State.SEARCH && state != State.EDIT)
-                || mShowMenuItemsWhileSearching;
-        mMenuItemsContainer.setVisibility(showButtons ? VISIBLE : GONE);
-        mOverflowButton.setVisible(showButtons && countVisibleOverflowItems() > 0);
+        mController.setState(state);
     }
 
     /** Gets the current {@link State} of the toolbar. */
     public State getState() {
-        return mState;
+        return mController.getState();
     }
 
     @Override
@@ -891,67 +537,67 @@
      */
     public void registerToolbarHeightChangeListener(
             OnHeightChangedListener listener) {
-        mOnHeightChangedListeners.add(listener);
+        mController.registerToolbarHeightChangeListener(listener);
     }
 
     /** Unregisters an existing {@link OnHeightChangedListener} from the list of listeners. */
     public boolean unregisterToolbarHeightChangeListener(
             OnHeightChangedListener listener) {
-        return mOnHeightChangedListeners.remove(listener);
+        return mController.unregisterToolbarHeightChangeListener(listener);
     }
 
     /** Registers a new {@link OnTabSelectedListener} to the list of listeners. */
     public void registerOnTabSelectedListener(OnTabSelectedListener listener) {
-        mOnTabSelectedListeners.add(listener);
+        mController.registerOnTabSelectedListener(listener);
     }
 
     /** Unregisters an existing {@link OnTabSelectedListener} from the list of listeners. */
     public boolean unregisterOnTabSelectedListener(OnTabSelectedListener listener) {
-        return mOnTabSelectedListeners.remove(listener);
+        return mController.unregisterOnTabSelectedListener(listener);
     }
 
     /** Registers a new {@link OnSearchListener} to the list of listeners. */
     public void registerOnSearchListener(OnSearchListener listener) {
-        mOnSearchListeners.add(listener);
+        mController.registerOnSearchListener(listener);
     }
 
     /** Unregisters an existing {@link OnSearchListener} from the list of listeners. */
     public boolean unregisterOnSearchListener(OnSearchListener listener) {
-        return mOnSearchListeners.remove(listener);
+        return mController.unregisterOnSearchListener(listener);
     }
 
     /** Registers a new {@link OnSearchCompletedListener} to the list of listeners. */
     public void registerOnSearchCompletedListener(OnSearchCompletedListener listener) {
-        mOnSearchCompletedListeners.add(listener);
+        mController.registerOnSearchCompletedListener(listener);
     }
 
     /** Unregisters an existing {@link OnSearchCompletedListener} from the list of listeners. */
     public boolean unregisterOnSearchCompletedListener(OnSearchCompletedListener listener) {
-        return mOnSearchCompletedListeners.remove(listener);
+        return mController.unregisterOnSearchCompletedListener(listener);
     }
 
     /** Registers a new {@link OnBackListener} to the list of listeners. */
     public void registerOnBackListener(OnBackListener listener) {
-        mOnBackListeners.add(listener);
+        mController.registerOnBackListener(listener);
     }
 
-    /** Unregisters an existing {@link OnTabSelectedListener} from the list of listeners. */
+    /** Unregisters an existing {@link OnBackListener} from the list of listeners. */
     public boolean unregisterOnBackListener(OnBackListener listener) {
-        return mOnBackListeners.remove(listener);
+        return mController.unregisterOnBackListener(listener);
     }
 
     /** Shows the progress bar */
     public void showProgressBar() {
-        mProgressBar.setVisibility(View.VISIBLE);
+        mController.showProgressBar();
     }
 
     /** Hides the progress bar */
     public void hideProgressBar() {
-        mProgressBar.setVisibility(View.GONE);
+        mController.hideProgressBar();
     }
 
     /** Returns the progress bar */
     public ProgressBar getProgressBar() {
-        return mProgressBar;
+        return mController.getProgressBar();
     }
 }
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/ToolbarController.java b/car-ui-lib/src/com/android/car/ui/toolbar/ToolbarController.java
new file mode 100644
index 0000000..2f70ab5
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/ToolbarController.java
@@ -0,0 +1,264 @@
+/*
+ * 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.toolbar;
+
+import android.graphics.drawable.Drawable;
+import android.widget.ProgressBar;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.XmlRes;
+
+import java.util.List;
+
+/**
+ * An interface for accessing a Chassis Toolbar, regardless of how the underlying
+ * views are represented.
+ */
+public interface ToolbarController {
+
+    /**
+     * Returns {@code true} if a two row layout in enabled for the toolbar.
+     */
+    boolean isTabsInSecondRow();
+
+    /**
+     * Sets the title of the toolbar to a string resource.
+     *
+     * <p>The title may not always be shown, for example with one row layout with tabs.
+     */
+    void setTitle(@StringRes int title);
+
+    /**
+     * Sets the title of the toolbar to a CharSequence.
+     *
+     * <p>The title may not always be shown, for example with one row layout with tabs.
+     */
+    void setTitle(CharSequence title);
+
+    /**
+     * Gets the current toolbar title.
+     */
+    CharSequence getTitle();
+
+    /**
+     * Gets the {@link TabLayout} for this toolbar.
+     */
+    TabLayout getTabLayout();
+
+    /**
+     * Adds a tab to this toolbar. You can listen for when it is selected via
+     * {@link #registerOnTabSelectedListener(Toolbar.OnTabSelectedListener)}.
+     */
+    void addTab(TabLayout.Tab tab);
+
+    /** Removes all the tabs. */
+    void clearAllTabs();
+
+    /**
+     * Gets a tab added to this toolbar. See
+     * {@link #addTab(TabLayout.Tab)}.
+     */
+    TabLayout.Tab getTab(int position);
+
+    /**
+     * Selects a tab added to this toolbar. See
+     * {@link #addTab(TabLayout.Tab)}.
+     */
+    void selectTab(int position);
+
+    /**
+     * Sets whether or not tabs should also be shown in the SUBPAGE {@link Toolbar.State}.
+     */
+    void setShowTabsInSubpage(boolean showTabs);
+
+    /**
+     * Gets whether or not tabs should also be shown in the SUBPAGE {@link Toolbar.State}.
+     */
+    boolean getShowTabsInSubpage();
+
+    /**
+     * Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo
+     * will be displayed next to the title.
+     */
+    void setLogo(@DrawableRes int resId);
+
+    /**
+     * Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo
+     * will be displayed next to the title.
+     */
+    void setLogo(Drawable drawable);
+
+    /** Sets the hint for the search bar. */
+    void setSearchHint(@StringRes int resId);
+
+    /** Sets the hint for the search bar. */
+    void setSearchHint(CharSequence hint);
+
+    /** Gets the search hint */
+    CharSequence getSearchHint();
+
+    /**
+     * Sets the icon to display in the search box.
+     *
+     * <p>The icon will be lost on configuration change, make sure to set it in onCreate() or
+     * a similar place.
+     */
+    void setSearchIcon(@DrawableRes int resId);
+
+    /**
+     * Sets the icon to display in the search box.
+     *
+     * <p>The icon will be lost on configuration change, make sure to set it in onCreate() or
+     * a similar place.
+     */
+    void setSearchIcon(Drawable d);
+
+
+    /** Sets the {@link Toolbar.NavButtonMode} */
+    void setNavButtonMode(Toolbar.NavButtonMode style);
+
+    /** Gets the {@link Toolbar.NavButtonMode} */
+    Toolbar.NavButtonMode getNavButtonMode();
+
+    /** Show/hide the background. When hidden, the toolbar is completely transparent. */
+    void setBackgroundShown(boolean shown);
+
+    /** Returns true is the toolbar background is shown */
+    boolean getBackgroundShown();
+
+    /**
+     * Sets the {@link MenuItem Menuitems} to display.
+     */
+    void setMenuItems(@Nullable List<MenuItem> items);
+
+    /**
+     * Sets the {@link MenuItem Menuitems} to display to a list defined in XML.
+     *
+     * <p>If this method is called twice with the same argument (and {@link #setMenuItems(List)}
+     * wasn't called), nothing will happen the second time, even if the MenuItems were changed.
+     *
+     * <p>The XML file must have one <MenuItems> tag, with a variable number of <MenuItem>
+     * child tags. See CarUiToolbarMenuItem in CarUi's attrs.xml for a list of available attributes.
+     *
+     * Example:
+     * <pre>
+     * <MenuItems>
+     *     <MenuItem
+     *         app:title="Foo"/>
+     *     <MenuItem
+     *         app:title="Bar"
+     *         app:icon="@drawable/ic_tracklist"
+     *         app:onClick="xmlMenuItemClicked"/>
+     *     <MenuItem
+     *         app:title="Bar"
+     *         app:checkable="true"
+     *         app:uxRestrictions="FULLY_RESTRICTED"
+     *         app:onClick="xmlMenuItemClicked"/>
+     * </MenuItems>
+     * </pre>
+     *
+     * @return The MenuItems that were loaded from XML.
+     * @see #setMenuItems(List)
+     */
+    List<MenuItem> setMenuItems(@XmlRes int resId);
+
+    /** Gets the {@link MenuItem MenuItems} currently displayed */
+    @NonNull
+    List<MenuItem> getMenuItems();
+
+    /** Gets a {@link MenuItem} by id. */
+    @Nullable
+    MenuItem findMenuItemById(int id);
+
+    /** Gets a {@link MenuItem} by id. Will throw an IllegalArgumentException if not found. */
+    @NonNull
+    MenuItem requireMenuItemById(int id);
+
+    /**
+     * Set whether or not to show the {@link MenuItem MenuItems} while searching. Default false.
+     * Even if this is set to true, the {@link MenuItem} created by
+     * {@link MenuItem.Builder#setToSearch()} will still be hidden.
+     */
+    void setShowMenuItemsWhileSearching(boolean showMenuItems);
+
+    /** Returns if {@link MenuItem MenuItems} are shown while searching */
+    boolean getShowMenuItemsWhileSearching();
+
+    /**
+     * Sets the search query.
+     */
+    void setSearchQuery(String query);
+
+    /**
+     * Sets the state of the toolbar. This will show/hide the appropriate elements of the toolbar
+     * for the desired state.
+     */
+    void setState(Toolbar.State state);
+
+    /** Gets the current {@link Toolbar.State} of the toolbar. */
+    Toolbar.State getState();
+
+    /**
+     * Registers a new {@link Toolbar.OnHeightChangedListener} to the list of listeners. Register a
+     * {@link com.android.car.ui.recyclerview.CarUiRecyclerView} only if there is a toolbar at
+     * the top and a {@link com.android.car.ui.recyclerview.CarUiRecyclerView} in the view and
+     * nothing else. {@link com.android.car.ui.recyclerview.CarUiRecyclerView} will
+     * automatically adjust its height according to the height of the Toolbar.
+     */
+    void registerToolbarHeightChangeListener(Toolbar.OnHeightChangedListener listener);
+
+    /** Unregisters an existing {@link Toolbar.OnHeightChangedListener} from the list of
+     * listeners. */
+    boolean unregisterToolbarHeightChangeListener(Toolbar.OnHeightChangedListener listener);
+
+    /** Registers a new {@link Toolbar.OnTabSelectedListener} to the list of listeners. */
+    void registerOnTabSelectedListener(Toolbar.OnTabSelectedListener listener);
+
+    /** Unregisters an existing {@link Toolbar.OnTabSelectedListener} from the list of listeners. */
+    boolean unregisterOnTabSelectedListener(Toolbar.OnTabSelectedListener listener);
+
+    /** Registers a new {@link Toolbar.OnSearchListener} to the list of listeners. */
+    void registerOnSearchListener(Toolbar.OnSearchListener listener);
+
+    /** Unregisters an existing {@link Toolbar.OnSearchListener} from the list of listeners. */
+    boolean unregisterOnSearchListener(Toolbar.OnSearchListener listener);
+
+    /** Registers a new {@link Toolbar.OnSearchCompletedListener} to the list of listeners. */
+    void registerOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener);
+
+    /** Unregisters an existing {@link Toolbar.OnSearchCompletedListener} from the list of
+     * listeners. */
+    boolean unregisterOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener);
+
+    /** Registers a new {@link Toolbar.OnBackListener} to the list of listeners. */
+    void registerOnBackListener(Toolbar.OnBackListener listener);
+
+    /** Unregisters an existing {@link Toolbar.OnBackListener} from the list of listeners. */
+    boolean unregisterOnBackListener(Toolbar.OnBackListener listener);
+
+    /** Shows the progress bar */
+    void showProgressBar();
+
+    /** Hides the progress bar */
+    void hideProgressBar();
+
+    /** Returns the progress bar */
+    ProgressBar getProgressBar();
+}
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/ToolbarControllerImpl.java b/car-ui-lib/src/com/android/car/ui/toolbar/ToolbarControllerImpl.java
new file mode 100644
index 0000000..eb49186
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/ToolbarControllerImpl.java
@@ -0,0 +1,767 @@
+/*
+ * 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.toolbar;
+
+import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.XmlRes;
+
+import com.android.car.ui.R;
+import com.android.car.ui.utils.CarUiUtils;
+import com.android.car.ui.utils.CarUxRestrictionsUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The implementation of {@link ToolbarController}. This class takes a ViewGroup, and looks
+ * in the ViewGroup to find all the toolbar-related views to control.
+ */
+public class ToolbarControllerImpl implements ToolbarController {
+    private static final String TAG = "CarUiToolbarController";
+
+    private View mBackground;
+    private ImageView mNavIcon;
+    private ImageView mLogoInNavIconSpace;
+    private ViewGroup mNavIconContainer;
+    private TextView mTitle;
+    private ImageView mTitleLogo;
+    private ViewGroup mTitleLogoContainer;
+    private TabLayout mTabLayout;
+    private ViewGroup mMenuItemsContainer;
+    private FrameLayout mSearchViewContainer;
+    private SearchView mSearchView;
+
+
+    // Cached values that we will send to views when they are inflated
+    private CharSequence mSearchHint;
+    private Drawable mSearchIcon;
+    private String mSearchQuery;
+    private final Context mContext;
+    private final Set<Toolbar.OnSearchListener> mOnSearchListeners = new HashSet<>();
+    private final Set<Toolbar.OnSearchCompletedListener> mOnSearchCompletedListeners =
+            new HashSet<>();
+
+    private final Set<Toolbar.OnBackListener> mOnBackListeners = new HashSet<>();
+    private final Set<Toolbar.OnTabSelectedListener> mOnTabSelectedListeners = new HashSet<>();
+    private final Set<Toolbar.OnHeightChangedListener> mOnHeightChangedListeners = new HashSet<>();
+
+    private final MenuItem mOverflowButton;
+    private final boolean mIsTabsInSecondRow;
+    private boolean mShowTabsInSubpage = false;
+    private boolean mHasLogo = false;
+    private boolean mShowMenuItemsWhileSearching;
+    private Toolbar.State mState = Toolbar.State.HOME;
+    private Toolbar.NavButtonMode mNavButtonMode = Toolbar.NavButtonMode.BACK;
+    @NonNull
+    private List<MenuItem> mMenuItems = Collections.emptyList();
+    private List<MenuItem> mOverflowItems = new ArrayList<>();
+    private final List<MenuItemRenderer> mMenuItemRenderers = new ArrayList<>();
+    private View[] mMenuItemViews;
+    private int mMenuItemsXmlId = 0;
+    private AlertDialog mOverflowDialog;
+    private boolean mNavIconSpaceReserved;
+    private boolean mLogoFillsNavIconSpace;
+    private boolean mShowLogo;
+    private ProgressBar mProgressBar;
+    private MenuItem.Listener mOverflowItemListener = () -> {
+        createOverflowDialog();
+        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();
+        mOverflowButton = MenuItem.builder(getContext())
+                .setIcon(R.drawable.car_ui_icon_overflow_menu)
+                .setTitle(R.string.car_ui_toolbar_menu_item_overflow_title)
+                .setOnClickListener(v -> {
+                    if (mOverflowDialog == null) {
+                        if (Log.isLoggable(TAG, Log.ERROR)) {
+                            Log.e(TAG, "Overflow dialog was null when trying to show it!");
+                        }
+                    } else {
+                        mOverflowDialog.show();
+                    }
+                })
+                .build();
+
+        mIsTabsInSecondRow = getContext().getResources().getBoolean(
+                R.bool.car_ui_toolbar_tabs_on_second_row);
+        mNavIconSpaceReserved = getContext().getResources().getBoolean(
+                R.bool.car_ui_toolbar_nav_icon_reserve_space);
+        mLogoFillsNavIconSpace = getContext().getResources().getBoolean(
+                R.bool.car_ui_toolbar_logo_fills_nav_icon_space);
+        mShowLogo = getContext().getResources().getBoolean(
+                R.bool.car_ui_toolbar_show_logo);
+
+        mBackground = requireViewByRefId(view, R.id.car_ui_toolbar_background);
+        mTabLayout = requireViewByRefId(view, R.id.car_ui_toolbar_tabs);
+        mNavIcon = requireViewByRefId(view, R.id.car_ui_toolbar_nav_icon);
+        mLogoInNavIconSpace = requireViewByRefId(view, R.id.car_ui_toolbar_logo);
+        mNavIconContainer = requireViewByRefId(view, R.id.car_ui_toolbar_nav_icon_container);
+        mMenuItemsContainer = requireViewByRefId(view, R.id.car_ui_toolbar_menu_items_container);
+        mTitle = requireViewByRefId(view, R.id.car_ui_toolbar_title);
+        mTitleLogoContainer = requireViewByRefId(view, R.id.car_ui_toolbar_title_logo_container);
+        mTitleLogo = requireViewByRefId(view, R.id.car_ui_toolbar_title_logo);
+        mSearchViewContainer = requireViewByRefId(view, R.id.car_ui_toolbar_search_view_container);
+        mProgressBar = requireViewByRefId(view, R.id.car_ui_toolbar_progress_bar);
+
+        mTabLayout.addListener(new TabLayout.Listener() {
+            @Override
+            public void onTabSelected(TabLayout.Tab tab) {
+                for (Toolbar.OnTabSelectedListener listener : mOnTabSelectedListeners) {
+                    listener.onTabSelected(tab);
+                }
+            }
+        });
+
+        mBackground.addOnLayoutChangeListener((v, left, top, right, bottom,
+                oldLeft, oldTop, oldRight, oldBottom) -> {
+            if (oldBottom - oldTop != bottom - top) {
+                for (Toolbar.OnHeightChangedListener listener : mOnHeightChangedListeners) {
+                    listener.onHeightChanged(mBackground.getHeight());
+                }
+            }
+        });
+
+        setBackgroundShown(true);
+
+        // This holds weak references so we don't need to unregister later
+        CarUxRestrictionsUtil.getInstance(getContext())
+                .register(mOnUxRestrictionsChangedListener);
+    }
+
+    private Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * Returns {@code true} if a two row layout in enabled for the toolbar.
+     */
+    public boolean isTabsInSecondRow() {
+        return mIsTabsInSecondRow;
+    }
+
+    /**
+     * Sets the title of the toolbar to a string resource.
+     *
+     * <p>The title may not always be shown, for example with one row layout with tabs.
+     */
+    public void setTitle(@StringRes int title) {
+        mTitle.setText(title);
+        setState(getState());
+    }
+
+    /**
+     * Sets the title of the toolbar to a CharSequence.
+     *
+     * <p>The title may not always be shown, for example with one row layout with tabs.
+     */
+    public void setTitle(CharSequence title) {
+        mTitle.setText(title);
+        setState(getState());
+    }
+
+    public CharSequence getTitle() {
+        return mTitle.getText();
+    }
+
+    /**
+     * Gets the {@link TabLayout} for this toolbar.
+     */
+    public TabLayout getTabLayout() {
+        return mTabLayout;
+    }
+
+    /**
+     * Adds a tab to this toolbar. You can listen for when it is selected via
+     * {@link #registerOnTabSelectedListener(Toolbar.OnTabSelectedListener)}.
+     */
+    public void addTab(TabLayout.Tab tab) {
+        mTabLayout.addTab(tab);
+        setState(getState());
+    }
+
+    /** Removes all the tabs. */
+    public void clearAllTabs() {
+        mTabLayout.clearAllTabs();
+        setState(getState());
+    }
+
+    /**
+     * Gets a tab added to this toolbar. See
+     * {@link #addTab(TabLayout.Tab)}.
+     */
+    public TabLayout.Tab getTab(int position) {
+        return mTabLayout.get(position);
+    }
+
+    /**
+     * Selects a tab added to this toolbar. See
+     * {@link #addTab(TabLayout.Tab)}.
+     */
+    public void selectTab(int position) {
+        mTabLayout.selectTab(position);
+    }
+
+    /**
+     * Sets whether or not tabs should also be shown in the SUBPAGE {@link Toolbar.State}.
+     */
+    public void setShowTabsInSubpage(boolean showTabs) {
+        if (showTabs != mShowTabsInSubpage) {
+            mShowTabsInSubpage = showTabs;
+            setState(getState());
+        }
+    }
+
+    /**
+     * Gets whether or not tabs should also be shown in the SUBPAGE {@link Toolbar.State}.
+     */
+    public boolean getShowTabsInSubpage() {
+        return mShowTabsInSubpage;
+    }
+
+    /**
+     * Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo
+     * will be displayed next to the title.
+     */
+    public void setLogo(@DrawableRes int resId) {
+        setLogo(resId != 0 ? getContext().getDrawable(resId) : null);
+    }
+
+    /**
+     * Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo
+     * will be displayed next to the title.
+     */
+    public void setLogo(Drawable drawable) {
+        if (!mShowLogo) {
+            // If no logo should be shown then we act as if we never received one.
+            return;
+        }
+        if (drawable != null) {
+            mLogoInNavIconSpace.setImageDrawable(drawable);
+            mTitleLogo.setImageDrawable(drawable);
+            mHasLogo = true;
+        } else {
+            mHasLogo = false;
+        }
+        setState(mState);
+    }
+
+    /** Sets the hint for the search bar. */
+    public void setSearchHint(@StringRes int resId) {
+        setSearchHint(getContext().getString(resId));
+    }
+
+    /** Sets the hint for the search bar. */
+    public void setSearchHint(CharSequence hint) {
+        mSearchHint = hint;
+        if (mSearchView != null) {
+            mSearchView.setHint(mSearchHint);
+        }
+    }
+
+    /** Gets the search hint */
+    public CharSequence getSearchHint() {
+        return mSearchHint;
+    }
+
+    /**
+     * Sets the icon to display in the search box.
+     *
+     * <p>The icon will be lost on configuration change, make sure to set it in onCreate() or
+     * a similar place.
+     */
+    public void setSearchIcon(@DrawableRes int resId) {
+        setSearchIcon(getContext().getDrawable(resId));
+    }
+
+    /**
+     * Sets the icon to display in the search box.
+     *
+     * <p>The icon will be lost on configuration change, make sure to set it in onCreate() or
+     * a similar place.
+     */
+    public void setSearchIcon(Drawable d) {
+        if (!Objects.equals(d, mSearchIcon)) {
+            mSearchIcon = d;
+            if (mSearchView != null) {
+                mSearchView.setIcon(mSearchIcon);
+            }
+        }
+    }
+
+
+    /** Sets the {@link Toolbar.NavButtonMode} */
+    public void setNavButtonMode(Toolbar.NavButtonMode style) {
+        if (style != mNavButtonMode) {
+            mNavButtonMode = style;
+            setState(mState);
+        }
+    }
+
+    /** Gets the {@link Toolbar.NavButtonMode} */
+    public Toolbar.NavButtonMode getNavButtonMode() {
+        return mNavButtonMode;
+    }
+
+    /** Show/hide the background. When hidden, the toolbar is completely transparent. */
+    public void setBackgroundShown(boolean shown) {
+        if (shown) {
+            mBackground.setBackground(
+                    getContext().getDrawable(R.drawable.car_ui_toolbar_background));
+        } else {
+            mBackground.setBackground(null);
+        }
+    }
+
+    /** Returns true is the toolbar background is shown */
+    public boolean getBackgroundShown() {
+        return mBackground.getBackground() != null;
+    }
+
+    private void setMenuItemsInternal(@Nullable List<MenuItem> items) {
+        if (items == null) {
+            items = Collections.emptyList();
+        }
+
+        List<MenuItem> visibleMenuItems = new ArrayList<>();
+        List<MenuItem> overflowItems = new ArrayList<>();
+        AtomicInteger loadedMenuItems = new AtomicInteger(0);
+
+        synchronized (this) {
+            if (items.equals(mMenuItems)) {
+                return;
+            }
+
+            for (MenuItem item : items) {
+                if (item.getDisplayBehavior() == MenuItem.DisplayBehavior.NEVER) {
+                    overflowItems.add(item);
+                    item.setListener(mOverflowItemListener);
+                } else {
+                    visibleMenuItems.add(item);
+                }
+            }
+
+            // Copy the list so that if the list is modified and setMenuItems is called again,
+            // the equals() check will fail. Note that the MenuItems are not copied here.
+            mMenuItems = new ArrayList<>(items);
+            mOverflowItems = overflowItems;
+            mMenuItemRenderers.clear();
+            mMenuItemsContainer.removeAllViews();
+
+            if (!overflowItems.isEmpty()) {
+                visibleMenuItems.add(mOverflowButton);
+                createOverflowDialog();
+            }
+
+            View[] menuItemViews = new View[visibleMenuItems.size()];
+            mMenuItemViews = menuItemViews;
+
+            for (int i = 0; i < visibleMenuItems.size(); ++i) {
+                int index = i;
+                MenuItem item = visibleMenuItems.get(i);
+                MenuItemRenderer renderer = new MenuItemRenderer(item, mMenuItemsContainer);
+                mMenuItemRenderers.add(renderer);
+                renderer.createView(view -> {
+                    synchronized (ToolbarControllerImpl.this) {
+                        if (menuItemViews != mMenuItemViews) {
+                            return;
+                        }
+
+                        menuItemViews[index] = view;
+                        if (loadedMenuItems.addAndGet(1) == menuItemViews.length) {
+                            for (View v : menuItemViews) {
+                                mMenuItemsContainer.addView(v);
+                            }
+                        }
+                    }
+                });
+            }
+        }
+
+        setState(mState);
+    }
+
+    /**
+     * Sets the {@link MenuItem Menuitems} to display.
+     */
+    public void setMenuItems(@Nullable List<MenuItem> items) {
+        mMenuItemsXmlId = 0;
+        setMenuItemsInternal(items);
+    }
+
+    /**
+     * Sets the {@link MenuItem Menuitems} to display to a list defined in XML.
+     *
+     * <p>If this method is called twice with the same argument (and {@link #setMenuItems(List)}
+     * wasn't called), nothing will happen the second time, even if the MenuItems were changed.
+     *
+     * <p>The XML file must have one <MenuItems> tag, with a variable number of <MenuItem>
+     * child tags. See CarUiToolbarMenuItem in CarUi's attrs.xml for a list of available attributes.
+     *
+     * Example:
+     * <pre>
+     * <MenuItems>
+     *     <MenuItem
+     *         app:title="Foo"/>
+     *     <MenuItem
+     *         app:title="Bar"
+     *         app:icon="@drawable/ic_tracklist"
+     *         app:onClick="xmlMenuItemClicked"/>
+     *     <MenuItem
+     *         app:title="Bar"
+     *         app:checkable="true"
+     *         app:uxRestrictions="FULLY_RESTRICTED"
+     *         app:onClick="xmlMenuItemClicked"/>
+     * </MenuItems>
+     * </pre>
+     *
+     * @return The MenuItems that were loaded from XML.
+     * @see #setMenuItems(List)
+     */
+    public List<MenuItem> setMenuItems(@XmlRes int resId) {
+        if (mMenuItemsXmlId != 0 && mMenuItemsXmlId == resId) {
+            return mMenuItems;
+        }
+
+        mMenuItemsXmlId = resId;
+        List<MenuItem> menuItems = MenuItemRenderer.readMenuItemList(getContext(), resId);
+        setMenuItemsInternal(menuItems);
+        return menuItems;
+    }
+
+    /** Gets the {@link MenuItem MenuItems} currently displayed */
+    @NonNull
+    public List<MenuItem> getMenuItems() {
+        return Collections.unmodifiableList(mMenuItems);
+    }
+
+    /** Gets a {@link MenuItem} by id. */
+    @Nullable
+    public MenuItem findMenuItemById(int id) {
+        for (MenuItem item : mMenuItems) {
+            if (item.getId() == id) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    /** Gets a {@link MenuItem} by id. Will throw an IllegalArgumentException if not found. */
+    @NonNull
+    public MenuItem requireMenuItemById(int id) {
+        MenuItem result = findMenuItemById(id);
+
+        if (result == null) {
+            throw new IllegalArgumentException("ID does not reference a MenuItem on this Toolbar");
+        }
+
+        return result;
+    }
+
+    private int countVisibleOverflowItems() {
+        int numVisibleItems = 0;
+        for (MenuItem item : mOverflowItems) {
+            if (item.isVisible()) {
+                numVisibleItems++;
+            }
+        }
+        return numVisibleItems;
+    }
+
+    private void createOverflowDialog() {
+        // TODO(b/140564530) Use a carui alert with a (car ui)recyclerview here
+        // TODO(b/140563930) Support enabled/disabled overflow items
+
+        CharSequence[] itemTitles = new CharSequence[countVisibleOverflowItems()];
+        int i = 0;
+        for (MenuItem item : mOverflowItems) {
+            if (item.isVisible()) {
+                itemTitles[i++] = item.getTitle();
+            }
+        }
+
+        mOverflowDialog = new AlertDialog.Builder(getContext())
+                .setItems(itemTitles, (dialog, which) -> {
+                    MenuItem item = mOverflowItems.get(which);
+                    MenuItem.OnClickListener listener = item.getOnClickListener();
+                    if (listener != null) {
+                        listener.onClick(item);
+                    }
+                })
+                .create();
+    }
+
+
+    /**
+     * Set whether or not to show the {@link MenuItem MenuItems} while searching. Default false.
+     * Even if this is set to true, the {@link MenuItem} created by
+     * {@link MenuItem.Builder#setToSearch()} will still be hidden.
+     */
+    public void setShowMenuItemsWhileSearching(boolean showMenuItems) {
+        mShowMenuItemsWhileSearching = showMenuItems;
+        setState(mState);
+    }
+
+    /** Returns if {@link MenuItem MenuItems} are shown while searching */
+    public boolean getShowMenuItemsWhileSearching() {
+        return mShowMenuItemsWhileSearching;
+    }
+
+    /**
+     * Sets the search query.
+     */
+    public void setSearchQuery(String query) {
+        if (mSearchView != null) {
+            mSearchView.setSearchQuery(query);
+        } else {
+            mSearchQuery = query;
+            for (Toolbar.OnSearchListener listener : mOnSearchListeners) {
+                listener.onSearch(query);
+            }
+        }
+    }
+
+    /**
+     * Sets the state of the toolbar. This will show/hide the appropriate elements of the toolbar
+     * for the desired state.
+     */
+    public void setState(Toolbar.State state) {
+        mState = state;
+
+        if (mSearchView == null && (state == Toolbar.State.SEARCH || state == Toolbar.State.EDIT)) {
+            SearchView searchView = new SearchView(getContext());
+            searchView.setHint(mSearchHint);
+            searchView.setIcon(mSearchIcon);
+            searchView.setSearchQuery(mSearchQuery);
+            searchView.setSearchListeners(mOnSearchListeners);
+            searchView.setSearchCompletedListeners(mOnSearchCompletedListeners);
+            searchView.setVisibility(GONE);
+
+            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT);
+            mSearchViewContainer.addView(searchView, layoutParams);
+
+            mSearchView = searchView;
+        }
+
+        for (MenuItemRenderer renderer : mMenuItemRenderers) {
+            renderer.setToolbarState(mState);
+        }
+
+        View.OnClickListener backClickListener = (v) -> {
+            boolean absorbed = false;
+            List<Toolbar.OnBackListener> listenersCopy = new ArrayList<>(mOnBackListeners);
+            for (Toolbar.OnBackListener listener : listenersCopy) {
+                absorbed = absorbed || listener.onBack();
+            }
+
+            if (!absorbed) {
+                Activity activity = CarUiUtils.getActivity(getContext());
+                if (activity != null) {
+                    activity.onBackPressed();
+                }
+            }
+        };
+
+        if (state == Toolbar.State.SEARCH) {
+            mNavIcon.setImageResource(R.drawable.car_ui_icon_search_nav_icon);
+        } else {
+            switch (mNavButtonMode) {
+                case CLOSE:
+                    mNavIcon.setImageResource(R.drawable.car_ui_icon_close);
+                    break;
+                case DOWN:
+                    mNavIcon.setImageResource(R.drawable.car_ui_icon_down);
+                    break;
+                default:
+                    mNavIcon.setImageResource(R.drawable.car_ui_icon_arrow_back);
+                    break;
+            }
+        }
+
+        mNavIcon.setVisibility(state != Toolbar.State.HOME ? VISIBLE : GONE);
+
+        // Show the logo in the nav space if that's enabled, we have a logo,
+        // and we're in the Home state.
+        mLogoInNavIconSpace.setVisibility(mHasLogo
+                && state == Toolbar.State.HOME
+                && mLogoFillsNavIconSpace
+                ? VISIBLE : INVISIBLE);
+
+        // Show logo next to the title if we're in the subpage state or we're configured to not show
+        // the logo in the nav icon space.
+        mTitleLogoContainer.setVisibility(mHasLogo
+                && (state == Toolbar.State.SUBPAGE
+                || (state == Toolbar.State.HOME && !mLogoFillsNavIconSpace))
+                ? VISIBLE : GONE);
+
+        // Show the nav icon container if we're not in the home space or the logo fills the nav icon
+        // container. If car_ui_toolbar_nav_icon_reserve_space is true, hiding it will still reserve
+        // its space
+        mNavIconContainer.setVisibility(
+                state != Toolbar.State.HOME || (mHasLogo && mLogoFillsNavIconSpace)
+                        ? VISIBLE : (mNavIconSpaceReserved ? INVISIBLE : GONE));
+        mNavIconContainer.setOnClickListener(
+                state != Toolbar.State.HOME ? backClickListener : null);
+        mNavIconContainer.setClickable(state != Toolbar.State.HOME);
+
+        boolean hasTabs = mTabLayout.getTabCount() > 0
+                && (state == Toolbar.State.HOME
+                || (state == Toolbar.State.SUBPAGE && mShowTabsInSubpage));
+        // Show the title if we're in the subpage state, or in the home state with no tabs or tabs
+        // on the second row
+        mTitle.setVisibility((state == Toolbar.State.SUBPAGE || state == Toolbar.State.HOME)
+                && (!hasTabs || mIsTabsInSecondRow)
+                ? VISIBLE : GONE);
+        mTabLayout.setVisibility(hasTabs ? VISIBLE : GONE);
+
+        if (mSearchView != null) {
+            if (state == Toolbar.State.SEARCH || state == Toolbar.State.EDIT) {
+                mSearchView.setPlainText(state == Toolbar.State.EDIT);
+                mSearchView.setVisibility(VISIBLE);
+            } else {
+                mSearchView.setVisibility(GONE);
+            }
+        }
+
+        boolean showButtons = (state != Toolbar.State.SEARCH && state != Toolbar.State.EDIT)
+                || mShowMenuItemsWhileSearching;
+        mMenuItemsContainer.setVisibility(showButtons ? VISIBLE : GONE);
+        mOverflowButton.setVisible(showButtons && countVisibleOverflowItems() > 0);
+    }
+
+    /** Gets the current {@link Toolbar.State} of the toolbar. */
+    public Toolbar.State getState() {
+        return mState;
+    }
+
+
+    /**
+     * Registers a new {@link Toolbar.OnHeightChangedListener} to the list of listeners. Register a
+     * {@link com.android.car.ui.recyclerview.CarUiRecyclerView} only if there is a toolbar at
+     * the top and a {@link com.android.car.ui.recyclerview.CarUiRecyclerView} in the view and
+     * nothing else. {@link com.android.car.ui.recyclerview.CarUiRecyclerView} will
+     * automatically adjust its height according to the height of the Toolbar.
+     */
+    public void registerToolbarHeightChangeListener(
+            Toolbar.OnHeightChangedListener listener) {
+        mOnHeightChangedListeners.add(listener);
+    }
+
+    /**
+     * Unregisters an existing {@link Toolbar.OnHeightChangedListener} from the list of
+     * listeners.
+     */
+    public boolean unregisterToolbarHeightChangeListener(
+            Toolbar.OnHeightChangedListener listener) {
+        return mOnHeightChangedListeners.remove(listener);
+    }
+
+    /** Registers a new {@link Toolbar.OnTabSelectedListener} to the list of listeners. */
+    public void registerOnTabSelectedListener(Toolbar.OnTabSelectedListener listener) {
+        mOnTabSelectedListeners.add(listener);
+    }
+
+    /** Unregisters an existing {@link Toolbar.OnTabSelectedListener} from the list of listeners. */
+    public boolean unregisterOnTabSelectedListener(Toolbar.OnTabSelectedListener listener) {
+        return mOnTabSelectedListeners.remove(listener);
+    }
+
+    /** Registers a new {@link Toolbar.OnSearchListener} to the list of listeners. */
+    public void registerOnSearchListener(Toolbar.OnSearchListener listener) {
+        mOnSearchListeners.add(listener);
+    }
+
+    /** Unregisters an existing {@link Toolbar.OnSearchListener} from the list of listeners. */
+    public boolean unregisterOnSearchListener(Toolbar.OnSearchListener listener) {
+        return mOnSearchListeners.remove(listener);
+    }
+
+    /** Registers a new {@link Toolbar.OnSearchCompletedListener} to the list of listeners. */
+    public void registerOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener) {
+        mOnSearchCompletedListeners.add(listener);
+    }
+
+    /**
+     * Unregisters an existing {@link Toolbar.OnSearchCompletedListener} from the list of
+     * listeners.
+     */
+    public boolean unregisterOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener) {
+        return mOnSearchCompletedListeners.remove(listener);
+    }
+
+    /** Registers a new {@link Toolbar.OnBackListener} to the list of listeners. */
+    public void registerOnBackListener(Toolbar.OnBackListener listener) {
+        mOnBackListeners.add(listener);
+    }
+
+    /** Unregisters an existing {@link Toolbar.OnBackListener} from the list of listeners. */
+    public boolean unregisterOnBackListener(Toolbar.OnBackListener listener) {
+        return mOnBackListeners.remove(listener);
+    }
+
+    /** Shows the progress bar */
+    public void showProgressBar() {
+        mProgressBar.setVisibility(View.VISIBLE);
+    }
+
+    /** Hides the progress bar */
+    public void hideProgressBar() {
+        mProgressBar.setVisibility(View.GONE);
+    }
+
+    /** Returns the progress bar */
+    public ProgressBar getProgressBar() {
+        return mProgressBar;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java b/car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
index 5802a7a..54d64f1 100644
--- a/car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
+++ b/car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
@@ -21,10 +21,14 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.util.TypedValue;
+import android.view.View;
 
 import androidx.annotation.DimenRes;
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StyleRes;
+import androidx.annotation.UiThread;
 
 /**
  * Collection of utility methods
@@ -78,4 +82,40 @@
         }
         return null;
     }
+
+    /**
+     * It behaves similar to @see View#findViewById, except it resolves the ID reference first.
+     *
+     * @param id the ID to search for
+     * @return a view with given ID if found, or {@code null} otherwise
+     * @see View#requireViewById(int)
+     */
+    @Nullable
+    @UiThread
+    public static <T extends View> T findViewByRefId(@NonNull View root, @IdRes int id) {
+        if (id == View.NO_ID) {
+            return null;
+        }
+
+        TypedValue value = new TypedValue();
+        root.getResources().getValue(id, value, true);
+        return root.findViewById(value.resourceId);
+    }
+
+    /**
+     * It behaves similar to @see View#requireViewById, except it resolves the ID reference first.
+     *
+     * @param id the ID to search for
+     * @return a view with given ID
+     * @see View#findViewById(int)
+     */
+    @NonNull
+    @UiThread
+    public static <T extends View> T requireViewByRefId(@NonNull View root, @IdRes int id) {
+        T view = findViewByRefId(root, id);
+        if (view == null) {
+            throw new IllegalArgumentException("ID does not reference a View inside this View");
+        }
+        return view;
+    }
 }
diff --git a/car-ui-lib/tests/apitest/current.xml b/car-ui-lib/tests/apitest/current.xml
index 5f21189..3856c54 100644
--- a/car-ui-lib/tests/apitest/current.xml
+++ b/car-ui-lib/tests/apitest/current.xml
@@ -8,7 +8,6 @@
   <public type="bool" name="car_ui_list_item_single_line_title"/>
   <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_above_recycler_view"/>
   <public type="bool" name="car_ui_scrollbar_enable"/>
   <public type="bool" name="car_ui_toolbar_logo_fills_nav_icon_space"/>
   <public type="bool" name="car_ui_toolbar_nav_icon_reserve_space"/>
@@ -59,11 +58,15 @@
   <public type="dimen" name="car_ui_letter_spacing_body3"/>
   <public type="dimen" name="car_ui_list_item_action_divider_height"/>
   <public type="dimen" name="car_ui_list_item_action_divider_width"/>
+  <public type="dimen" name="car_ui_list_item_avatar_icon_height"/>
+  <public type="dimen" name="car_ui_list_item_avatar_icon_width"/>
   <public type="dimen" name="car_ui_list_item_body_text_size"/>
   <public type="dimen" name="car_ui_list_item_check_box_end_inset"/>
   <public type="dimen" name="car_ui_list_item_check_box_height"/>
   <public type="dimen" name="car_ui_list_item_check_box_icon_container_width"/>
   <public type="dimen" name="car_ui_list_item_check_box_start_inset"/>
+  <public type="dimen" name="car_ui_list_item_content_icon_height"/>
+  <public type="dimen" name="car_ui_list_item_content_icon_width"/>
   <public type="dimen" name="car_ui_list_item_end_inset"/>
   <public type="dimen" name="car_ui_list_item_header_height"/>
   <public type="dimen" name="car_ui_list_item_header_start_inset"/>
@@ -165,9 +168,15 @@
   <public type="drawable" name="car_ui_icon_down"/>
   <public type="drawable" name="car_ui_icon_overflow_menu"/>
   <public type="drawable" name="car_ui_icon_search"/>
+  <public type="drawable" name="car_ui_icon_search_nav_icon"/>
   <public type="drawable" name="car_ui_icon_settings"/>
+  <public type="drawable" name="car_ui_list_header_background"/>
+  <public type="drawable" name="car_ui_list_item_avatar_icon_outline"/>
+  <public type="drawable" name="car_ui_list_item_background"/>
   <public type="drawable" name="car_ui_list_item_divider"/>
   <public type="drawable" name="car_ui_preference_icon_chevron"/>
+  <public type="drawable" name="car_ui_preference_icon_chevron_disabled"/>
+  <public type="drawable" name="car_ui_preference_icon_chevron_enabled"/>
   <public type="drawable" name="car_ui_recyclerview_button_ripple_background"/>
   <public type="drawable" name="car_ui_recyclerview_divider"/>
   <public type="drawable" name="car_ui_recyclerview_ic_down"/>
@@ -181,8 +190,6 @@
   <public type="drawable" name="car_ui_toolbar_search_search_icon"/>
   <public type="id" name="search"/>
   <public type="integer" name="car_ui_default_max_string_length"/>
-  <public type="integer" name="car_ui_scrollbar_gutter"/>
-  <public type="integer" name="car_ui_scrollbar_position"/>
   <public type="string" name="car_ui_alert_dialog_default_button"/>
   <public type="string" name="car_ui_dialog_preference_negative"/>
   <public type="string" name="car_ui_dialog_preference_positive"/>
@@ -200,6 +207,7 @@
   <public type="string" name="car_ui_toolbar_menu_item_search_title"/>
   <public type="string" name="car_ui_toolbar_menu_item_settings_title"/>
   <public type="style" name="CarUiPreferenceTheme"/>
+  <public type="style" name="CarUiPreferenceTheme.WithToolbar"/>
   <public type="style" name="Preference.CarUi"/>
   <public type="style" name="Preference.CarUi.Category"/>
   <public type="style" name="Preference.CarUi.CheckBoxPreference"/>
@@ -213,9 +221,13 @@
   <public type="style" name="Preference.CarUi.SeekBarPreference"/>
   <public type="style" name="Preference.CarUi.SwitchPreference"/>
   <public type="style" name="PreferenceFragment.CarUi"/>
+  <public type="style" name="PreferenceFragment.CarUi.WithToolbar"/>
   <public type="style" name="PreferenceFragmentList.CarUi"/>
   <public type="style" name="TextAppearance.CarUi"/>
   <public type="style" name="TextAppearance.CarUi.AlertDialog.Subtitle"/>
+  <public type="style" name="TextAppearance.CarUi.Body1"/>
+  <public type="style" name="TextAppearance.CarUi.Body2"/>
+  <public type="style" name="TextAppearance.CarUi.Body3"/>
   <public type="style" name="TextAppearance.CarUi.ListItem"/>
   <public type="style" name="TextAppearance.CarUi.ListItem.Body"/>
   <public type="style" name="TextAppearance.CarUi.ListItem.Header"/>
@@ -229,6 +241,8 @@
   <public type="style" name="TextAppearance.CarUi.Widget.Toolbar.Tab.Selected"/>
   <public type="style" name="TextAppearance.CarUi.Widget.Toolbar.Title"/>
   <public type="style" name="Theme.CarUi"/>
+  <public type="style" name="Theme.CarUi.NoToolbar"/>
+  <public type="style" name="Theme.CarUi.WithToolbar"/>
   <public type="style" name="Widget.CarUi"/>
   <public type="style" name="Widget.CarUi.AlertDialog"/>
   <public type="style" name="Widget.CarUi.AlertDialog.HeaderContainer"/>
@@ -237,13 +251,16 @@
   <public type="style" name="Widget.CarUi.Button"/>
   <public type="style" name="Widget.CarUi.Button.Borderless.Colored"/>
   <public type="style" name="Widget.CarUi.CarUiRecyclerView"/>
-  <public type="style" name="Widget.CarUi.CarUiRecyclerView.NestedRecyclerView"/>
+  <public type="style" name="Widget.CarUi.SeekbarPreference"/>
+  <public type="style" name="Widget.CarUi.SeekbarPreference.Seekbar"/>
   <public type="style" name="Widget.CarUi.Toolbar"/>
   <public type="style" name="Widget.CarUi.Toolbar.BottomView"/>
   <public type="style" name="Widget.CarUi.Toolbar.Container"/>
   <public type="style" name="Widget.CarUi.Toolbar.Logo"/>
   <public type="style" name="Widget.CarUi.Toolbar.LogoContainer"/>
+  <public type="style" name="Widget.CarUi.Toolbar.MenuItem"/>
   <public type="style" name="Widget.CarUi.Toolbar.MenuItem.Container"/>
+  <public type="style" name="Widget.CarUi.Toolbar.MenuItem.IndividualContainer"/>
   <public type="style" name="Widget.CarUi.Toolbar.NavIcon"/>
   <public type="style" name="Widget.CarUi.Toolbar.NavIconContainer"/>
   <public type="style" name="Widget.CarUi.Toolbar.ProgressBar"/>
diff --git a/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml b/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml
index e16428f..61ca559 100644
--- a/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml
+++ b/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml
@@ -21,7 +21,7 @@
   <application
       android:icon="@drawable/ic_launcher"
       android:label="@string/app_name"
-      android:theme="@style/Theme.CarUi">
+      android:theme="@style/Theme.CarUi.WithToolbar">
     <activity
         android:name=".MainActivity"
         android:exported="true">
@@ -40,10 +40,6 @@
         android:exported="false"
         android:parentActivityName=".MainActivity"/>
     <activity
-        android:name=".caruirecyclerview.CarUiListItemActivity"
-        android:exported="false"
-        android:parentActivityName=".MainActivity"/>
-    <activity
         android:name=".caruirecyclerview.GridCarUiRecyclerViewActivity"
         android:exported="false"
         android:parentActivityName=".MainActivity"/>
@@ -57,5 +53,25 @@
         android:parentActivityName=".MainActivity">
       <meta-data android:name="distractionOptimized" android:value="true"/>
     </activity>
+    <activity
+        android:name=".overlays.OverlayActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity">
+      <meta-data android:name="distractionOptimized" android:value="true"/>
+    </activity>
+    <activity
+        android:name=".widgets.WidgetActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".caruirecyclerview.CarUiListItemActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+
+    <!-- Remove this on R, it's to workaround a bug in the Qt manifest merger -->
+    <provider
+        android:name="com.android.car.ui.core.CarUiInstaller"
+        android:authorities="${applicationId}.CarUiInstaller"
+        android:exported="false"/>
   </application>
 </manifest>
diff --git a/car-ui-lib/tests/paintbooth/AndroidManifest.xml b/car-ui-lib/tests/paintbooth/AndroidManifest.xml
index ca7dec7..ba222d2 100644
--- a/car-ui-lib/tests/paintbooth/AndroidManifest.xml
+++ b/car-ui-lib/tests/paintbooth/AndroidManifest.xml
@@ -30,7 +30,7 @@
   <application
       android:icon="@drawable/ic_launcher"
       android:label="@string/app_name"
-      android:theme="@style/Theme.CarUi">
+      android:theme="@style/Theme.CarUi.WithToolbar">
     <activity
         android:name=".MainActivity"
         android:exported="true">
@@ -76,5 +76,11 @@
         android:name=".caruirecyclerview.CarUiListItemActivity"
         android:exported="false"
         android:parentActivityName=".MainActivity"/>
+
+    <!-- Remove this on R, it's to workaround a bug in the Qt manifest merger -->
+    <provider
+        android:name="com.android.car.ui.core.CarUiInstaller"
+        android:authorities="${applicationId}.CarUiInstaller"
+        android:exported="false"/>
   </application>
 </manifest>
diff --git a/car-ui-lib/tests/paintbooth/res/drawable/ic_sample_logo.png b/car-ui-lib/tests/paintbooth/res/drawable/ic_sample_logo.png
new file mode 100644
index 0000000..a4f8245
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/drawable/ic_sample_logo.png
Binary files differ
diff --git a/car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_activity.xml b/car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_activity.xml
index 330c781..15b6fe6 100644
--- a/car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_activity.xml
+++ b/car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_activity.xml
@@ -14,25 +14,9 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<LinearLayout
+<com.android.car.ui.recyclerview.CarUiRecyclerView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/list"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:background="@drawable/car_ui_activity_background">
-
-    <com.android.car.ui.toolbar.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:title="@string/app_name"
-        app:logo="@drawable/ic_launcher"
-        app:state="subpage"/>
-
-    <com.android.car.ui.recyclerview.CarUiRecyclerView
-        android:id="@+id/list"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-
-</LinearLayout>
\ No newline at end of file
+    android:background="@drawable/car_ui_activity_background"/>
\ No newline at end of file
diff --git a/car-ui-lib/tests/paintbooth/res/layout/grid_car_ui_recycler_view_activity.xml b/car-ui-lib/tests/paintbooth/res/layout/grid_car_ui_recycler_view_activity.xml
index b472d06..5c97fde 100644
--- a/car-ui-lib/tests/paintbooth/res/layout/grid_car_ui_recycler_view_activity.xml
+++ b/car-ui-lib/tests/paintbooth/res/layout/grid_car_ui_recycler_view_activity.xml
@@ -14,24 +14,12 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.car.ui.recyclerview.CarUiRecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/list"
+    app:layoutStyle="grid"
+    app:numOfColumns="4"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:background="@drawable/car_ui_activity_background">
-
-  <com.android.car.ui.toolbar.Toolbar
-      android:id="@+id/toolbar"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      app:title="@string/app_name"
-      app:state="subpage"/>
-
-  <com.android.car.ui.recyclerview.CarUiRecyclerView
-      android:id="@+id/grid_list"
-      app:layoutStyle="grid"
-      app:numOfColumns="4"
-      android:layout_width="match_parent"
-      android:layout_height="match_parent" />
-</LinearLayout>
+    android:background="@drawable/car_ui_activity_background" />
\ No newline at end of file
diff --git a/car-ui-lib/tests/paintbooth/res/layout/main_activity.xml b/car-ui-lib/tests/paintbooth/res/layout/main_activity.xml
deleted file mode 100644
index 6642b20..0000000
--- a/car-ui-lib/tests/paintbooth/res/layout/main_activity.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?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:id="@+id/home"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="@drawable/car_ui_activity_background">
-
-  <com.android.car.ui.toolbar.Toolbar
-      android:id="@+id/toolbar"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      app:layout_constraintTop_toTopOf="parent"
-      app:title="@string/app_name"
-      app:logo="@drawable/ic_launcher"/>
-
-  <com.android.car.ui.recyclerview.CarUiRecyclerView
-      android:id="@+id/activities"
-      android:layout_width="0dp"
-      android:layout_height="0dp"
-      app:layout_constraintLeft_toLeftOf="parent"
-      app:layout_constraintRight_toRightOf="parent"
-      app:layout_constraintTop_toBottomOf="@id/toolbar"
-      app:layout_constraintBottom_toBottomOf="parent"/>
-
-</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/tests/paintbooth/res/layout/widgets_activity.xml b/car-ui-lib/tests/paintbooth/res/layout/widgets_activity.xml
index b282921..dcadcea 100644
--- a/car-ui-lib/tests/paintbooth/res/layout/widgets_activity.xml
+++ b/car-ui-lib/tests/paintbooth/res/layout/widgets_activity.xml
@@ -15,18 +15,10 @@
   ~ limitations under the License.
   -->
 <LinearLayout 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="match_parent"
               android:orientation="vertical">
 
-    <com.android.car.ui.toolbar.Toolbar
-        android:id="@+id/toolbar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:title="@string/app_name"
-        app:state="subpage"/>
-
     <CheckBox
         android:id="@+id/check"
         android:layout_width="wrap_content"
diff --git a/car-ui-lib/tests/paintbooth/res/values/strings.xml b/car-ui-lib/tests/paintbooth/res/values/strings.xml
index 8132ef3..cd45f77 100644
--- a/car-ui-lib/tests/paintbooth/res/values/strings.xml
+++ b/car-ui-lib/tests/paintbooth/res/values/strings.xml
@@ -209,6 +209,9 @@
   <!-- Text for add tab with custom text button [CHAR_LIMIT=40]-->
   <string name="toolbar_add_tab_with_custom_text">Add tab with custom text</string>
 
+  <!-- Text for showing tabs in subpages [CHAR_LIMIT=50]-->
+  <string name="toolbar_show_tabs_in_subpage">Toggle showing tabs in subpages</string>
+
   <!-- Text for toggle search icon button [CHAR_LIMIT=30]-->
   <string name="toolbar_toggle_search_icon">Toggle search icon</string>
 
@@ -245,6 +248,9 @@
   <!-- Button that shows a dialog with a subtitle and icon [CHAR_LIMIT=50]-->
   <string name="dialog_show_subtitle_and_icon">Show Dialog with title, subtitle, and icon</string>
 
+  <!-- Text to show Dialog with single choice items-->
+  <string name="dialog_show_single_choice">Show with single choice items</string>
+
   <!--This section is for widget attributes -->
   <eat-comment/>
   <!-- Text for checkbox [CHAR_LIMIT=16]-->
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/MainActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/MainActivity.java
index f05a827..3cf2b9f 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/MainActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/MainActivity.java
@@ -30,6 +30,9 @@
 import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.CarUi;
 import com.android.car.ui.paintbooth.caruirecyclerview.CarUiListItemActivity;
 import com.android.car.ui.paintbooth.caruirecyclerview.CarUiRecyclerViewActivity;
 import com.android.car.ui.paintbooth.caruirecyclerview.GridCarUiRecyclerViewActivity;
@@ -39,6 +42,7 @@
 import com.android.car.ui.paintbooth.toolbar.ToolbarActivity;
 import com.android.car.ui.paintbooth.widgets.WidgetActivity;
 import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.toolbar.ToolbarController;
 
 import java.lang.reflect.Method;
 import java.util.Arrays;
@@ -47,7 +51,7 @@
 /**
  * Paint booth app
  */
-public class MainActivity extends Activity {
+public class MainActivity extends Activity implements InsetsChangedListener {
 
     /**
      * List of all sample activities.
@@ -105,9 +109,13 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.main_activity);
+        setContentView(R.layout.car_ui_recycler_view_activity);
 
-        CarUiRecyclerView prv = findViewById(R.id.activities);
+        ToolbarController toolbar = CarUi.requireToolbar(this);
+        toolbar.setLogo(R.drawable.ic_launcher);
+        toolbar.setTitle(getTitle());
+
+        CarUiRecyclerView prv = findViewById(R.id.list);
         prv.setAdapter(mAdapter);
 
         initLeakCanary();
@@ -189,4 +197,12 @@
             // LeakCanary is not used in this build, do nothing.
         }
     }
+
+    @Override
+    public void onCarUiInsetsChanged(Insets insets) {
+        requireViewById(R.id.list)
+                .setPadding(0, insets.getTop(), 0, insets.getBottom());
+        requireViewById(android.R.id.content)
+                .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
+    }
 }
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
index a5aeeee..927b3f4 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
@@ -21,20 +21,24 @@
 import android.os.Bundle;
 import android.widget.Toast;
 
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.CarUi;
 import com.android.car.ui.paintbooth.R;
 import com.android.car.ui.recyclerview.CarUiContentListItem;
 import com.android.car.ui.recyclerview.CarUiHeaderListItem;
 import com.android.car.ui.recyclerview.CarUiListItem;
 import com.android.car.ui.recyclerview.CarUiListItemAdapter;
-import com.android.car.ui.recyclerview.CarUiListItemLayoutManager;
 import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
 
 import java.util.ArrayList;
 
 /**
  * Activity that shows {@link CarUiRecyclerView} with dummy {@link CarUiContentListItem} entries
  */
-public class CarUiListItemActivity extends Activity {
+public class CarUiListItemActivity extends Activity implements InsetsChangedListener {
 
     private final ArrayList<CarUiListItem> mData = new ArrayList<>();
     private CarUiListItemAdapter mAdapter;
@@ -43,11 +47,14 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.car_ui_recycler_view_activity);
-        CarUiRecyclerView recyclerView = findViewById(R.id.list);
 
+        ToolbarController toolbar = CarUi.requireToolbar(this);
+        toolbar.setTitle(getTitle());
+        toolbar.setState(Toolbar.State.SUBPAGE);
+
+        CarUiRecyclerView recyclerView = findViewById(R.id.list);
         mAdapter = new CarUiListItemAdapter(generateDummyData());
         recyclerView.setAdapter(mAdapter);
-        recyclerView.setLayoutManager(new CarUiListItemLayoutManager(this));
     }
 
     private ArrayList<CarUiListItem> generateDummyData() {
@@ -56,65 +63,97 @@
         CarUiHeaderListItem header = new CarUiHeaderListItem("First header");
         mData.add(header);
 
-        CarUiContentListItem item = new CarUiContentListItem();
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
         item.setTitle("Test title");
         item.setBody("Test body");
         mData.add(item);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
         item.setTitle("Test title with no body");
         mData.add(item);
 
         header = new CarUiHeaderListItem("Random header", "with header body");
         mData.add(header);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
         item.setBody("Test body with no title");
         mData.add(item);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
         item.setTitle("Test Title");
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         mData.add(item);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
         item.setTitle("Test Title");
         item.setBody("Test body text");
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         mData.add(item);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+        item.setTitle("Test Title -- with content icon");
+        item.setPrimaryIconType(CarUiContentListItem.IconType.CONTENT);
+        item.setIcon(getDrawable(R.drawable.ic_sample_logo));
+        mData.add(item);
+
+        item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+        item.setTitle("Test Title");
+        item.setBody("With avatar icon.");
+        item.setIcon(getDrawable(R.drawable.ic_sample_logo));
+        item.setPrimaryIconType(CarUiContentListItem.IconType.AVATAR);
+
+        item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+        item.setTitle("Test Title");
+        item.setBody("Displays toast on click");
+        item.setIcon(getDrawable(R.drawable.ic_launcher));
+        item.setOnItemClickedListener(item1 -> {
+            Toast.makeText(context, "Item clicked", Toast.LENGTH_SHORT).show();
+        });
+        mData.add(item);
+
+        item = new CarUiContentListItem(CarUiContentListItem.Action.CHECK_BOX);
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         item.setTitle("Title -- Item with checkbox");
         item.setBody("Will present toast on change of selection state.");
-        item.setOnCheckedChangedListener(
+        item.setOnCheckedChangeListener(
                 (listItem, isChecked) -> Toast.makeText(context,
                         "Item checked state is: " + isChecked, Toast.LENGTH_SHORT).show());
-        item.setAction(CarUiContentListItem.Action.CHECK_BOX);
         mData.add(item);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.CHECK_BOX);
         item.setIcon(getDrawable(R.drawable.ic_launcher));
-        item.setBody("Body -- Item with switch");
-        item.setAction(CarUiContentListItem.Action.SWITCH);
+        item.setEnabled(false);
+        item.setTitle("Title -- Checkbox that is disabled");
+        item.setBody("Clicks should not have any affect");
+        item.setOnCheckedChangeListener(
+                (listItem, isChecked) -> Toast.makeText(context,
+                        "Item checked state is: " + isChecked, Toast.LENGTH_SHORT).show());
         mData.add(item);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.SWITCH);
+        item.setIcon(getDrawable(R.drawable.ic_launcher));
+        item.setBody("Body -- Item with switch  -- with click listener");
+        item.setOnItemClickedListener(item1 -> {
+            Toast.makeText(context, "Click on item with switch", Toast.LENGTH_SHORT).show();
+        });
+        mData.add(item);
+
+        item = new CarUiContentListItem(CarUiContentListItem.Action.CHECK_BOX);
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         item.setTitle("Title -- Item with checkbox");
         item.setBody("Item is initially checked");
-        item.setAction(CarUiContentListItem.Action.CHECK_BOX);
         item.setChecked(true);
         mData.add(item);
 
-        CarUiContentListItem radioItem1 = new CarUiContentListItem();
-        CarUiContentListItem radioItem2 = new CarUiContentListItem();
+        CarUiContentListItem radioItem1 = new CarUiContentListItem(
+                CarUiContentListItem.Action.RADIO_BUTTON);
+        CarUiContentListItem radioItem2 = new CarUiContentListItem(
+                CarUiContentListItem.Action.RADIO_BUTTON);
 
         radioItem1.setTitle("Title -- Item with radio button");
         radioItem1.setBody("Item is initially unchecked checked");
-        radioItem1.setAction(CarUiContentListItem.Action.RADIO_BUTTON);
         radioItem1.setChecked(false);
-        radioItem1.setOnCheckedChangedListener((listItem, isChecked) -> {
+        radioItem1.setOnCheckedChangeListener((listItem, isChecked) -> {
             if (isChecked) {
                 radioItem2.setChecked(false);
                 mAdapter.notifyItemChanged(mData.indexOf(radioItem2));
@@ -124,9 +163,8 @@
 
         radioItem2.setIcon(getDrawable(R.drawable.ic_launcher));
         radioItem2.setTitle("Item is mutually exclusive with item above");
-        radioItem2.setAction(CarUiContentListItem.Action.RADIO_BUTTON);
         radioItem2.setChecked(true);
-        radioItem2.setOnCheckedChangedListener((listItem, isChecked) -> {
+        radioItem2.setOnCheckedChangeListener((listItem, isChecked) -> {
             if (isChecked) {
                 radioItem1.setChecked(false);
                 mAdapter.notifyItemChanged(mData.indexOf(radioItem1));
@@ -134,7 +172,7 @@
         });
         mData.add(radioItem2);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.ICON);
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         item.setTitle("Title");
         item.setBody("Random body text -- with action divider");
@@ -143,14 +181,13 @@
         item.setChecked(true);
         mData.add(item);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.ICON);
         item.setIcon(getDrawable(R.drawable.ic_launcher));
         item.setTitle("Null supplemental icon");
-        item.setAction(CarUiContentListItem.Action.ICON);
         item.setChecked(true);
         mData.add(item);
 
-        item = new CarUiContentListItem();
+        item = new CarUiContentListItem(CarUiContentListItem.Action.ICON);
         item.setTitle("Supplemental icon with listener");
         item.setSupplementalIcon(getDrawable(R.drawable.ic_launcher),
                 v -> Toast.makeText(context, "Clicked supplemental icon",
@@ -160,4 +197,12 @@
 
         return mData;
     }
+
+    @Override
+    public void onCarUiInsetsChanged(Insets insets) {
+        requireViewById(R.id.list)
+                .setPadding(0, insets.getTop(), 0, insets.getBottom());
+        requireViewById(android.R.id.content)
+                .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
+    }
 }
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiRecyclerViewActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiRecyclerViewActivity.java
index a05b39d..a3ac9c2 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiRecyclerViewActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiRecyclerViewActivity.java
@@ -21,15 +21,20 @@
 
 import androidx.recyclerview.widget.LinearLayoutManager;
 
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.CarUi;
 import com.android.car.ui.paintbooth.R;
 import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
 
 import java.util.ArrayList;
 
 /**
  * Activity that shows CarUiRecyclerView example with dummy data.
  */
-public class CarUiRecyclerViewActivity extends Activity {
+public class CarUiRecyclerViewActivity extends Activity implements InsetsChangedListener {
     private final ArrayList<String> mData = new ArrayList<>();
     private final int mDataToGenerate = 100;
 
@@ -37,6 +42,11 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.car_ui_recycler_view_activity);
+
+        ToolbarController toolbar = CarUi.requireToolbar(this);
+        toolbar.setTitle(getTitle());
+        toolbar.setState(Toolbar.State.SUBPAGE);
+
         CarUiRecyclerView recyclerView = findViewById(R.id.list);
         recyclerView.setLayoutManager(new LinearLayoutManager(this));
 
@@ -50,5 +60,13 @@
         }
         return mData;
     }
+
+    @Override
+    public void onCarUiInsetsChanged(Insets insets) {
+        requireViewById(R.id.list)
+                .setPadding(0, insets.getTop(), 0, insets.getBottom());
+        requireViewById(android.R.id.content)
+                .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
+    }
 }
 
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/GridCarUiRecyclerViewActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/GridCarUiRecyclerViewActivity.java
index 928f4d7..586b633 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/GridCarUiRecyclerViewActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/GridCarUiRecyclerViewActivity.java
@@ -19,13 +19,19 @@
 import android.app.Activity;
 import android.os.Bundle;
 
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.CarUi;
 import com.android.car.ui.paintbooth.R;
 import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
 
 import java.util.ArrayList;
 
 /** Activity that shows GridCarUiRecyclerView example with dummy data. */
-public class GridCarUiRecyclerViewActivity extends Activity {
+public class GridCarUiRecyclerViewActivity extends Activity implements
+        InsetsChangedListener {
     private final ArrayList<String> mData = new ArrayList<>();
     private final int mDataToGenerate = 200;
 
@@ -33,7 +39,12 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.grid_car_ui_recycler_view_activity);
-        CarUiRecyclerView recyclerView = findViewById(R.id.grid_list);
+
+        ToolbarController toolbar = CarUi.requireToolbar(this);
+        toolbar.setTitle(getTitle());
+        toolbar.setState(Toolbar.State.SUBPAGE);
+
+        CarUiRecyclerView recyclerView = findViewById(R.id.list);
 
         RecyclerViewAdapter adapter = new RecyclerViewAdapter(generateDummyData());
         recyclerView.setAdapter(adapter);
@@ -45,4 +56,12 @@
         }
         return mData;
     }
+
+    @Override
+    public void onCarUiInsetsChanged(Insets insets) {
+        requireViewById(R.id.list)
+                .setPadding(0, insets.getTop(), 0, insets.getBottom());
+        requireViewById(android.R.id.content)
+                .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
+    }
 }
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
index 37c9a42..1f5f26a 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
@@ -28,8 +28,15 @@
 import androidx.annotation.NonNull;
 
 import com.android.car.ui.AlertDialogBuilder;
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.CarUi;
 import com.android.car.ui.paintbooth.R;
+import com.android.car.ui.recyclerview.CarUiRadioButtonListItem;
+import com.android.car.ui.recyclerview.CarUiRadioButtonListItemAdapter;
 import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -37,7 +44,7 @@
 /**
  * Activity that shows different dialogs from the device default theme.
  */
-public class DialogsActivity extends Activity {
+public class DialogsActivity extends Activity implements InsetsChangedListener {
 
     private final List<Pair<Integer, View.OnClickListener>> mButtons = new ArrayList<>();
 
@@ -45,6 +52,9 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.car_ui_recycler_view_activity);
+        ToolbarController toolbar = CarUi.requireToolbar(this);
+        toolbar.setTitle(getTitle());
+        toolbar.setState(Toolbar.State.SUBPAGE);
 
         mButtons.add(Pair.create(R.string.dialog_show_dialog,
                 v -> showDialog()));
@@ -66,6 +76,9 @@
                 v -> showDialogWithSubtitle()));
         mButtons.add(Pair.create(R.string.dialog_show_subtitle_and_icon,
                 v -> showDialogWithSubtitleAndIcon()));
+        mButtons.add(Pair.create(R.string.dialog_show_single_choice,
+                v -> showDialogWithSingleChoiceItems()));
+
 
         CarUiRecyclerView recyclerView = requireViewById(R.id.list);
         recyclerView.setAdapter(mAdapter);
@@ -152,6 +165,28 @@
                 .show();
     }
 
+    private void showDialogWithSingleChoiceItems() {
+        ArrayList<CarUiRadioButtonListItem> data = new ArrayList<>();
+
+        CarUiRadioButtonListItem item = new CarUiRadioButtonListItem();
+        item.setTitle("First item");
+        data.add(item);
+
+        item = new CarUiRadioButtonListItem();
+        item.setTitle("Second item");
+        data.add(item);
+
+        item = new CarUiRadioButtonListItem();
+        item.setTitle("Third item");
+        data.add(item);
+
+        new AlertDialogBuilder(this)
+                .setTitle("Select one option.")
+                .setSubtitle("Ony one option may be selected at a time")
+                .setSingleChoiceItems(new CarUiRadioButtonListItemAdapter(data), null)
+                .show();
+    }
+
     private void showDialogWithSubtitleAndIcon() {
         new AlertDialogBuilder(this)
                 .setTitle("My Title!")
@@ -197,4 +232,12 @@
                     holder.bind(pair.first, pair.second);
                 }
             };
+
+    @Override
+    public void onCarUiInsetsChanged(Insets insets) {
+        requireViewById(R.id.list)
+                .setPadding(0, insets.getTop(), 0, insets.getBottom());
+        requireViewById(android.R.id.content)
+                .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
+    }
 }
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
index 04649f8..cb1fdc2 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
@@ -15,7 +15,6 @@
  */
 package com.android.car.ui.paintbooth.toolbar;
 
-import android.app.Activity;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.text.Editable;
@@ -29,20 +28,26 @@
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.ui.AlertDialogBuilder;
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.baselayout.InsetsChangedListener;
+import com.android.car.ui.core.CarUi;
 import com.android.car.ui.paintbooth.R;
 import com.android.car.ui.recyclerview.CarUiRecyclerView;
 import com.android.car.ui.toolbar.MenuItem;
 import com.android.car.ui.toolbar.TabLayout;
 import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
 
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-public class ToolbarActivity extends Activity {
+public class ToolbarActivity extends AppCompatActivity implements InsetsChangedListener {
 
     private List<MenuItem> mMenuItems = new ArrayList<>();
     private List<Pair<CharSequence, View.OnClickListener>> mButtons = new ArrayList<>();
@@ -52,7 +57,10 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.car_ui_recycler_view_activity);
 
-        Toolbar toolbar = requireViewById(R.id.toolbar);
+        ToolbarController toolbar = CarUi.requireToolbar(this);
+        toolbar.setTitle(getTitle());
+        toolbar.setState(Toolbar.State.SUBPAGE);
+        toolbar.setLogo(R.drawable.ic_launcher);
         toolbar.registerOnBackListener(
                 () -> {
                     if (toolbar.getState() == Toolbar.State.SEARCH
@@ -270,6 +278,9 @@
                     .show();
         }));
 
+        mButtons.add(Pair.create(getString(R.string.toolbar_show_tabs_in_subpage), v ->
+                toolbar.setShowTabsInSubpage(!toolbar.getShowTabsInSubpage())));
+
         Mutable<Boolean> showingLauncherIcon = new Mutable<>(false);
         mButtons.add(Pair.create(getString(R.string.toolbar_toggle_search_icon), v -> {
             if (showingLauncherIcon.value) {
@@ -284,6 +295,14 @@
         prv.setAdapter(mAdapter);
     }
 
+    @Override
+    public void onCarUiInsetsChanged(Insets insets) {
+        requireViewById(R.id.list)
+                .setPadding(0, insets.getTop(), 0, insets.getBottom());
+        requireViewById(android.R.id.content)
+                .setPadding(insets.getLeft(), 0, insets.getRight(), 0);
+    }
+
     public void xmlMenuItemClicked(MenuItem item) {
         Toast.makeText(this, "Xml item clicked! " + item.getTitle() + ", id: " + item.getId(),
                 Toast.LENGTH_SHORT).show();
@@ -312,7 +331,7 @@
                 }).show();
     }
 
-    private static class ViewHolder extends CarUiRecyclerView.ViewHolder {
+    private static class ViewHolder extends RecyclerView.ViewHolder {
 
         private final Button mButton;
 
@@ -327,8 +346,8 @@
         }
     }
 
-    private CarUiRecyclerView.Adapter<ViewHolder> mAdapter =
-            new CarUiRecyclerView.Adapter<ViewHolder>() {
+    private final RecyclerView.Adapter<ViewHolder> mAdapter =
+            new RecyclerView.Adapter<ViewHolder>() {
                 @Override
                 public int getItemCount() {
                     return mButtons.size();
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/widgets/WidgetActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/widgets/WidgetActivity.java
index c5c8c8c..ab97ae3 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/widgets/WidgetActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/widgets/WidgetActivity.java
@@ -19,7 +19,10 @@
 import android.app.Activity;
 import android.os.Bundle;
 
+import com.android.car.ui.core.CarUi;
 import com.android.car.ui.paintbooth.R;
+import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.toolbar.ToolbarController;
 
 /**
  * Activity that shows different widgets from the device default theme.
@@ -30,5 +33,8 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.widgets_activity);
+        ToolbarController toolbar = CarUi.requireToolbar(this);
+        toolbar.setTitle(getTitle());
+        toolbar.setState(Toolbar.State.SUBPAGE);
     }
 }
diff --git a/car-ui-lib/tests/robotests/build.gradle b/car-ui-lib/tests/robotests/build.gradle
index 8553359..b5876a3 100644
--- a/car-ui-lib/tests/robotests/build.gradle
+++ b/car-ui-lib/tests/robotests/build.gradle
@@ -20,10 +20,10 @@
     repositories {
         google()
         jcenter()
-
     }
+
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.5.0'
+        classpath 'com.android.tools.build:gradle:3.5.3'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
@@ -72,7 +72,6 @@
             includeAndroidResources = true
         }
     }
-
 }
 
 dependencies {
@@ -83,5 +82,6 @@
     testImplementation "com.google.truth:truth:0.29"
     testImplementation "org.testng:testng:6.9.9"
 
+    // This is the gradle equivalent of linking to android.car in our Android.mk
     implementation files('../../../../../../../out/target/common/obj/JAVA_LIBRARIES/android.car_intermediates/classes.jar')
 }
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiRobolectricTestRunner.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiRobolectricTestRunner.java
index 0a3474b..20561f2 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiRobolectricTestRunner.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiRobolectricTestRunner.java
@@ -50,9 +50,9 @@
 
     static {
         AAR_VERSIONS = new HashMap<>();
-        AAR_VERSIONS.put("appcompat", "1.1.0-alpha01");
+        AAR_VERSIONS.put("appcompat", "1.1.0-alpha06");
         AAR_VERSIONS.put("constraintlayout", "1.1.2");
-        AAR_VERSIONS.put("preference", "1.1.0-alpha02");
+        AAR_VERSIONS.put("preference", "1.1.0-alpha06");
     }
 
     public CarUiRobolectricTestRunner(Class<?> testClass) throws InitializationError {
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiTestUtil.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiTestUtil.java
new file mode 100644
index 0000000..407e3ef
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiTestUtil.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 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;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+
+import org.robolectric.RuntimeEnvironment;
+
+/**
+ * Collection of test utility methods
+ */
+public class CarUiTestUtil {
+
+    /**
+     * Returns a mocked {@link Context} to be used in Robolectric tests.
+     */
+    public static Context getMockContext() {
+        Context context = spy(RuntimeEnvironment.application);
+        Resources mResources = spy(context.getResources());
+
+        when(context.getResources()).thenReturn(mResources);
+
+        // Temporarily create a layout inflater that will be used to clone a new one.
+        LayoutInflater tempInflater = LayoutInflater.from(context);
+        // Force layout inflater to use spied context
+        doAnswer(invocation -> tempInflater.cloneInContext(context))
+                .when(context).getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        // Older versions of Robolectric do not correctly handle the Resources#getValue() method.
+        // This breaks CarUtils.findViewByRefId() functionality in tests. To workaround this issue,
+        // use a spy to rely on findViewById() functionality instead.
+        doAnswer(invocation -> {
+            Object[] args = invocation.getArguments();
+            ((TypedValue) args[1]).resourceId = (int) args[0];
+            return null; // void method, so return null
+        }).when(mResources).getValue(anyInt(), isA(TypedValue.class), isA(Boolean.class));
+        return context;
+    }
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiListItemTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiListItemTest.java
index dec4c19..f404f6c 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiListItemTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiListItemTest.java
@@ -28,6 +28,7 @@
 import android.widget.TextView;
 
 import com.android.car.ui.CarUiRobolectricTestRunner;
+import com.android.car.ui.CarUiTestUtil;
 import com.android.car.ui.R;
 import com.android.car.ui.TestConfig;
 
@@ -36,7 +37,6 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
 import java.util.ArrayList;
@@ -50,12 +50,12 @@
     private Context mContext;
 
     @Mock
-    CarUiContentListItem.OnCheckedChangedListener mOnCheckedChangedListener;
+    CarUiContentListItem.OnCheckedChangeListener mOnCheckedChangeListener;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mContext = RuntimeEnvironment.application;
+        mContext = CarUiTestUtil.getMockContext();
         mListView = new CarUiRecyclerView(mContext);
     }
 
@@ -114,9 +114,9 @@
         mListView.measure(0, 0);
         mListView.layout(0, 0, 100, 10000);
 
-        if (mListView.mNestedRecyclerView != null) {
-            mListView.mNestedRecyclerView.measure(0, 0);
-            mListView.mNestedRecyclerView.layout(0, 0, 100, 10000);
+        if (mListView != null) {
+            mListView.measure(0, 0);
+            mListView.layout(0, 0, 100, 10000);
         }
 
         // Required to init nested RecyclerView
@@ -127,7 +127,7 @@
     public void testItemVisibility_withTitle() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiContentListItem item = new CarUiContentListItem();
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
         item.setTitle("Test title");
         items.add(item);
 
@@ -145,7 +145,7 @@
     public void testItemVisibility_withTitle_withBody() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiContentListItem item = new CarUiContentListItem();
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
         item.setTitle("Test title");
         item.setBody("Test body");
         items.add(item);
@@ -164,7 +164,7 @@
     public void testItemVisibility_withTitle_withIcon() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiContentListItem item = new CarUiContentListItem();
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
         item.setTitle("Test title");
         item.setIcon(mContext.getDrawable(R.drawable.car_ui_icon_close));
         items.add(item);
@@ -183,9 +183,8 @@
     public void testItemVisibility_withTitle_withCheckbox() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiContentListItem item = new CarUiContentListItem();
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.CHECK_BOX);
         item.setTitle("Test title");
-        item.setAction(CarUiContentListItem.Action.CHECK_BOX);
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
@@ -204,10 +203,9 @@
     public void testItemVisibility_withTitle_withBody_withSwitch() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiContentListItem item = new CarUiContentListItem();
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.SWITCH);
         item.setTitle("Test title");
         item.setBody("Body text");
-        item.setAction(CarUiContentListItem.Action.SWITCH);
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
@@ -226,10 +224,9 @@
     public void testCheckedState_switch() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiContentListItem item = new CarUiContentListItem();
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.SWITCH);
         item.setTitle("Test title");
-        item.setOnCheckedChangedListener(mOnCheckedChangedListener);
-        item.setAction(CarUiContentListItem.Action.SWITCH);
+        item.setOnCheckedChangeListener(mOnCheckedChangeListener);
         item.setChecked(true);
         items.add(item);
 
@@ -240,7 +237,7 @@
         assertThat(switchWidget.isChecked()).isEqualTo(true);
         switchWidget.performClick();
         assertThat(switchWidget.isChecked()).isEqualTo(false);
-        verify(mOnCheckedChangedListener, times(1))
+        verify(mOnCheckedChangeListener, times(1))
                 .onCheckedChanged(item, false);
     }
 
@@ -248,10 +245,9 @@
     public void testCheckedState_checkbox() {
         List<CarUiListItem> items = new ArrayList<>();
 
-        CarUiContentListItem item = new CarUiContentListItem();
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.CHECK_BOX);
         item.setTitle("Test title");
-        item.setAction(CarUiContentListItem.Action.CHECK_BOX);
-        item.setOnCheckedChangedListener(mOnCheckedChangedListener);
+        item.setOnCheckedChangeListener(mOnCheckedChangeListener);
         items.add(item);
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
@@ -261,7 +257,7 @@
         assertThat(checkBox.isChecked()).isEqualTo(false);
         checkBox.performClick();
         assertThat(checkBox.isChecked()).isEqualTo(true);
-        verify(mOnCheckedChangedListener, times(1))
+        verify(mOnCheckedChangeListener, times(1))
                 .onCheckedChanged(item, true);
     }
 
@@ -275,8 +271,6 @@
 
         updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
 
-        CarUiListItemAdapter.HeaderViewHolder viewHolder = getHeaderViewHolderAtPosition(0);
-
         assertThat(getHeaderViewHolderTitleAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
         assertThat(getHeaderViewHolderTitleAtPosition(0).getText()).isEqualTo(title);
         assertThat(getHeaderViewHolderBodyAtPosition(0).getVisibility()).isNotEqualTo(View.VISIBLE);
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapterTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapterTest.java
index 67266e9..6c8954c 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapterTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapterTest.java
@@ -25,6 +25,7 @@
 import android.view.ViewGroup;
 
 import com.android.car.ui.CarUiRobolectricTestRunner;
+import com.android.car.ui.CarUiTestUtil;
 import com.android.car.ui.TestConfig;
 
 import org.junit.Before;
@@ -32,7 +33,6 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
 @RunWith(CarUiRobolectricTestRunner.class)
@@ -50,8 +50,7 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-
-        mContext = RuntimeEnvironment.application;
+        mContext = CarUiTestUtil.getMockContext();
         mCarUiRecyclerViewAdapter = new CarUiRecyclerViewAdapter();
     }
 
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
index 5cf9e85..f4d98f7 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
@@ -18,39 +18,33 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
-
 import android.content.Context;
-import android.content.res.Resources;
 import android.view.LayoutInflater;
 import android.view.View;
 
 import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.ui.CarUiRobolectricTestRunner;
 import com.android.car.ui.R;
+import com.android.car.ui.TestConfig;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.Robolectric;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
 
 @RunWith(CarUiRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
 public class CarUiRecyclerViewTest {
 
     private Context mContext;
     private View mView;
     private CarUiRecyclerView mCarUiRecyclerView;
 
-    @Mock
-    private RecyclerView.Adapter mAdapter;
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -101,25 +95,13 @@
     }
 
     @Test
-    public void init_shouldContainNestedRecyclerView() {
+    public void init_shouldContainRecyclerView() {
         mView = LayoutInflater.from(mContext)
                 .inflate(R.layout.test_grid_car_ui_recycler_view, null);
 
         mCarUiRecyclerView = mView.findViewById(R.id.test_prv);
 
-        assertThat(mCarUiRecyclerView.mNestedRecyclerView).isNotNull();
-    }
-
-    @Test
-    public void init_shouldNotContainNestedRecyclerView() {
-        Context context = spy(mContext);
-        Resources resources = spy(mContext.getResources());
-        when(resources.getBoolean(R.bool.car_ui_scrollbar_enable)).thenReturn(false);
-        when(context.getResources()).thenReturn(resources);
-
-        mCarUiRecyclerView = new CarUiRecyclerView(context);
-
-        assertThat(mCarUiRecyclerView.mNestedRecyclerView).isNull();
+        assertThat(mCarUiRecyclerView).isNotNull();
     }
 
     @Test
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSmoothScrollerTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSmoothScrollerTest.java
index eb54f31..052304c 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSmoothScrollerTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSmoothScrollerTest.java
@@ -23,13 +23,16 @@
 import android.content.Context;
 
 import com.android.car.ui.CarUiRobolectricTestRunner;
+import com.android.car.ui.TestConfig;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
 
 @RunWith(CarUiRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
 public class CarUiSmoothScrollerTest {
 
     private Context mContext;
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSnapHelperTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSnapHelperTest.java
index 1b07e78..b846103 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSnapHelperTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSnapHelperTest.java
@@ -19,29 +19,23 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
-import android.graphics.PointF;
 import android.view.View;
 
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.android.car.ui.CarUiRobolectricTestRunner;
-import com.android.car.ui.TestConfig;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
-import org.robolectric.annotation.Config;
 
-@RunWith(CarUiRobolectricTestRunner.class)
-@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+@RunWith(RobolectricTestRunner.class)
 public class CarUiSnapHelperTest {
 
     private Context mContext;
@@ -70,89 +64,6 @@
     }
 
     @Test
-    public void smoothScrollBy_invalidSnapPosition_shouldCallRecylerViewSmoothScrollBy() {
-        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
-
-        mCarUiSnapHelper.smoothScrollBy(10);
-
-        verify(mRecyclerView).smoothScrollBy(0, 10);
-    }
-
-    @Test
-    public void smoothScrollBy_invalidSnapPositionNoItem_shouldCallRecylerViewSmoothScrollBy() {
-        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
-        when(mLayoutManager.getItemCount()).thenReturn(0);
-
-        mCarUiSnapHelper.smoothScrollBy(10);
-
-        verify(mRecyclerView).smoothScrollBy(0, 10);
-    }
-
-    @Test
-    public void smoothScrollBy_invalidSnapPositionNoView_shouldCallRecylerViewSmoothScrollBy() {
-        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
-        when(mLayoutManager.getItemCount()).thenReturn(10);
-        when(mLayoutManager.canScrollVertically()).thenReturn(false);
-        when(mLayoutManager.canScrollHorizontally()).thenReturn(false);
-
-        mCarUiSnapHelper.smoothScrollBy(10);
-
-        verify(mRecyclerView).smoothScrollBy(0, 10);
-    }
-
-    @Test
-    public void smoothScrollBy_invalidSnapPositionNoVectore_shouldCallRecylerViewSmoothScrollBy() {
-        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
-        when(mLayoutManager.getItemCount()).thenReturn(10);
-        when(mLayoutManager.canScrollVertically()).thenReturn(true);
-        when(mLayoutManager.getChildCount()).thenReturn(1);
-        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
-        when(mLayoutManager.getChildAt(0)).thenReturn(mChild);
-
-        mCarUiSnapHelper.smoothScrollBy(10);
-
-        verify(mRecyclerView).smoothScrollBy(0, 10);
-    }
-
-    @Test
-    public void smoothScrollBy_invalidSnapPositionNoDelta_shouldCallRecylerViewSmoothScrollBy() {
-        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
-        when(mLayoutManager.getItemCount()).thenReturn(1);
-        when(mLayoutManager.canScrollVertically()).thenReturn(true);
-        when(mLayoutManager.getChildCount()).thenReturn(1);
-        // no delta
-        when(mLayoutManager.getDecoratedBottom(any())).thenReturn(0);
-        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
-        when(mLayoutManager.getChildAt(0)).thenReturn(mChild);
-
-        PointF vectorForEnd = new PointF(100, 100);
-        when(mLayoutManager.computeScrollVectorForPosition(0)).thenReturn(vectorForEnd);
-
-        mCarUiSnapHelper.smoothScrollBy(10);
-
-        verify(mRecyclerView).smoothScrollBy(0, 10);
-    }
-
-    @Test
-    public void smoothScrollBy_validSnapPosition_shouldCallRecylerViewSmoothScrollBy() {
-        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
-        when(mLayoutManager.getItemCount()).thenReturn(1);
-        when(mLayoutManager.canScrollVertically()).thenReturn(true);
-        when(mLayoutManager.getChildCount()).thenReturn(1);
-        // some delta
-        when(mLayoutManager.getDecoratedBottom(any())).thenReturn(10);
-        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
-        when(mLayoutManager.getChildAt(0)).thenReturn(mChild);
-
-        PointF vectorForEnd = new PointF(100, 100);
-        when(mLayoutManager.computeScrollVectorForPosition(0)).thenReturn(vectorForEnd);
-
-        mCarUiSnapHelper.smoothScrollBy(10);
-
-        verify(mLayoutManager).startSmoothScroll(any(RecyclerView.SmoothScroller.class));
-    }
-
-    @Test
     public void calculateDistanceToFinalSnap_shouldReturnTopMarginDifference() {
         when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
         when(mLayoutManager.getItemCount()).thenReturn(1);
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/DefaultScrollBarTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/DefaultScrollBarTest.java
index 65b01a8..612f463 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/DefaultScrollBarTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/DefaultScrollBarTest.java
@@ -25,11 +25,15 @@
 import static org.testng.Assert.assertThrows;
 
 import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
 import android.widget.FrameLayout;
 
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.car.ui.CarUiRobolectricTestRunner;
+import com.android.car.ui.CarUiTestUtil;
+import com.android.car.ui.R;
 import com.android.car.ui.TestConfig;
 
 import org.junit.Before;
@@ -37,7 +41,6 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
 @RunWith(CarUiRobolectricTestRunner.class)
@@ -59,8 +62,7 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mContext = RuntimeEnvironment.application;
-
+        mContext = CarUiTestUtil.getMockContext();
         mScrollBar = new DefaultScrollBar();
     }
 
@@ -71,7 +73,9 @@
         when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
         when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
 
-        mScrollBar.initialize(mRecyclerView, 10, CarUiRecyclerView.ScrollBarPosition.START, true);
+        View scrollView = LayoutInflater.from(mContext).inflate(
+                R.layout.car_ui_recyclerview_scrollbar, null);
+        mScrollBar.initialize(mRecyclerView, scrollView);
 
         // called once in DefaultScrollBar and once in SnapHelper while setting up the call backs
         // when we use attachToRecyclerView(recyclerview)
@@ -86,7 +90,9 @@
         when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
         when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
 
-        mScrollBar.initialize(mRecyclerView, 10, CarUiRecyclerView.ScrollBarPosition.START, true);
+        View scrollView = LayoutInflater.from(mContext).inflate(
+                R.layout.car_ui_recyclerview_scrollbar, null);
+        mScrollBar.initialize(mRecyclerView, scrollView);
 
         verify(mRecycledViewPool).setMaxRecycledViews(0, 12);
     }
@@ -98,7 +104,9 @@
         when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
         when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
 
-        mScrollBar.initialize(mRecyclerView, 10, CarUiRecyclerView.ScrollBarPosition.START, true);
+        View scrollView = LayoutInflater.from(mContext).inflate(
+                R.layout.car_ui_recyclerview_scrollbar, null);
+        mScrollBar.initialize(mRecyclerView, scrollView);
 
         verify(mRecyclerView).setOnFlingListener(null);
     }
@@ -110,7 +118,9 @@
         when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
         when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
 
-        mScrollBar.initialize(mRecyclerView, 10, CarUiRecyclerView.ScrollBarPosition.START, true);
+        View scrollView = LayoutInflater.from(mContext).inflate(
+                R.layout.car_ui_recyclerview_scrollbar, null);
+        mScrollBar.initialize(mRecyclerView, scrollView);
         mScrollBar.setPadding(10, 20);
 
         DefaultScrollBar defaultScrollBar = (DefaultScrollBar) mScrollBar;
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ExtendedShadowTypeface.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ExtendedShadowTypeface.java
new file mode 100644
index 0000000..a5bc1b1
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ExtendedShadowTypeface.java
@@ -0,0 +1,34 @@
+/*
+ * 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.toolbar;
+
+import android.graphics.Typeface;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowTypeface;
+
+@Implements(Typeface.class)
+public class ExtendedShadowTypeface extends ShadowTypeface {
+    @Implementation
+    protected static Typeface create(Typeface family, int weight, boolean italic) {
+        // Increment style by 10 to distinguish when a style has been italicized. This a workaround
+        // for ShadowTypeface not supporting italicization for Typeface.
+        int style = italic ? weight + 10 : weight;
+        return ShadowTypeface.create(family, style);
+    }
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ShadowAsyncLayoutInflater.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ShadowAsyncLayoutInflater.java
new file mode 100644
index 0000000..817ab97
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ShadowAsyncLayoutInflater.java
@@ -0,0 +1,45 @@
+/*
+ * 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.toolbar;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/**
+ * Shadow of {@link AsyncLayoutInflater} that inflates synchronously, so that tests
+ * don't have to have complicated code to wait for these inflations.
+ */
+@Implements(AsyncLayoutInflater.class)
+public class ShadowAsyncLayoutInflater {
+    @Implementation
+    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
+            @NonNull AsyncLayoutInflater.OnInflateFinishedListener callback) {
+        View result = LayoutInflater.from(parent.getContext())
+                .inflate(resid, parent, false);
+
+        callback.onInflateFinished(result, resid, parent);
+    }
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/TestActivity.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/TestActivity.java
deleted file mode 100644
index dcbe1cd..0000000
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/TestActivity.java
+++ /dev/null
@@ -1,43 +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.ui.toolbar;
-
-import android.app.Activity;
-import android.os.Bundle;
-
-import com.android.car.ui.R;
-
-/** An activity to use in the Toolbar tests */
-public class TestActivity extends Activity {
-
-    private int mTimesBackPressed = 0;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.test_toolbar);
-    }
-
-    @Override
-    public void onBackPressed() {
-        mTimesBackPressed++;
-        super.onBackPressed();
-    }
-
-    public int getTimesBackPressed() {
-        return mTimesBackPressed;
-    }
-}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java
index cea19e3..78aab7d 100644
--- a/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java
@@ -18,7 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
@@ -27,14 +26,13 @@
 import android.view.ViewGroup;
 
 import com.android.car.ui.CarUiRobolectricTestRunner;
+import com.android.car.ui.CarUiTestUtil;
 import com.android.car.ui.R;
+import com.android.car.ui.TestConfig;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.Robolectric;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.android.controller.ActivityController;
 import org.robolectric.annotation.Config;
 
 import java.util.Arrays;
@@ -42,23 +40,18 @@
 import java.util.List;
 
 @RunWith(CarUiRobolectricTestRunner.class)
-@Config(qualifiers = "land")
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+        shadows = {ExtendedShadowTypeface.class, ShadowAsyncLayoutInflater.class})
 public class ToolbarTest {
-
     private Context mContext;
     private Resources mResources;
-    private ActivityController<TestActivity> mActivityController;
-    private TestActivity mActivity;
     private Toolbar mToolbar;
 
     @Before
     public void setUp() {
-        mContext = RuntimeEnvironment.application;
+        mContext = CarUiTestUtil.getMockContext();
         mResources = mContext.getResources();
-        mActivityController = Robolectric.buildActivity(TestActivity.class);
-        mActivityController.setup();
-        mActivity = mActivityController.get();
-        mToolbar = mActivity.findViewById(R.id.toolbar);
+        mToolbar = new Toolbar(mContext);
     }
 
     @Test
@@ -100,21 +93,26 @@
         mToolbar.setState(Toolbar.State.HOME);
         mToolbar.setLogo(R.drawable.test_ic_launcher);
 
-        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_logo).isShown()).isTrue();
-        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo).isShown()).isFalse();
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_nav_icon_container).getVisibility())
+                .isEqualTo(View.VISIBLE);
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_logo).getVisibility())
+                .isEqualTo(View.VISIBLE);
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo_container).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
     }
 
     @Test
     public void hideLogo_andTitleLogo_whenSet_andStateIsHome_andLogoIsDisabled() {
-        mockResources();
         when(mResources.getBoolean(R.bool.car_ui_toolbar_show_logo)).thenReturn(false);
 
         Toolbar toolbar = new Toolbar(mContext);
         toolbar.setState(Toolbar.State.HOME);
         toolbar.setLogo(R.drawable.test_ic_launcher);
 
-        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_logo).isShown()).isFalse();
-        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_title_logo).isShown()).isFalse();
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_nav_icon_container).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo_container).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
     }
 
     @Test
@@ -122,8 +120,12 @@
         mToolbar.setState(Toolbar.State.SUBPAGE);
         mToolbar.setLogo(R.drawable.test_ic_launcher);
 
-        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_logo).isShown()).isFalse();
-        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo).isShown()).isTrue();
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_nav_icon_container).getVisibility())
+                .isEqualTo(View.VISIBLE);
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo_container).getVisibility())
+                .isEqualTo(View.VISIBLE);
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo).getVisibility())
+                .isEqualTo(View.VISIBLE);
     }
 
     @Test
@@ -131,17 +133,20 @@
         mToolbar.setState(Toolbar.State.HOME);
         mToolbar.setLogo(0);
 
-        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_logo).isShown()).isFalse();
-        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo).isShown()).isFalse();
-    }
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_nav_icon_container).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo_container).getVisibility())
+                .isNotEqualTo(View.VISIBLE);    }
 
     @Test
     public void hideLogo_andTitleLogo_whenNotSet_andStateIsNotHome() {
         mToolbar.setState(Toolbar.State.SUBPAGE);
         mToolbar.setLogo(0);
 
-        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_logo).isShown()).isFalse();
-        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo).isShown()).isFalse();
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_logo).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo_container).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
     }
 
     @Test
@@ -157,24 +162,10 @@
         pressBack();
 
         assertThat(timesBackPressed.value).isEqualTo(1);
-        assertThat(mActivity.getTimesBackPressed()).isEqualTo(1);
-    }
-
-    @Test
-    public void registerOnBackListener_whenAListenerReturnsTrue_shouldSuppressBack() {
-        mToolbar.setState(Toolbar.State.SUBPAGE);
-
-        mToolbar.registerOnBackListener(() -> true);
-        pressBack();
-        mToolbar.registerOnBackListener(() -> false);
-        pressBack();
-
-        assertThat(mActivity.getTimesBackPressed()).isEqualTo(0);
     }
 
     @Test
     public void testState_twoRow_withTitle_withTabs() {
-        mockResources();
         when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(true);
 
         Toolbar toolbar = new Toolbar(mContext);
@@ -193,8 +184,7 @@
     }
 
     @Test
-    public void testState_twoRow_withTitle()  {
-        mockResources();
+    public void testState_twoRow_withTitle() {
         when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(true);
 
         Toolbar toolbar = new Toolbar(mContext);
@@ -211,7 +201,6 @@
 
     @Test
     public void testState_twoRow_withTabs() {
-        mockResources();
         when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(true);
 
         Toolbar toolbar = new Toolbar(mContext);
@@ -228,7 +217,6 @@
 
     @Test
     public void testState_oneRow_withTitle_withTabs() {
-        mockResources();
         when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(false);
 
         Toolbar toolbar = new Toolbar(mContext);
@@ -248,10 +236,8 @@
 
     @Test
     public void testState_oneRow_withTitle() {
-        mockResources();
         when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(false);
 
-
         Toolbar toolbar = new Toolbar(mContext);
         assertThat(toolbar.isTabsInSecondRow()).isFalse();
 
@@ -266,7 +252,6 @@
 
     @Test
     public void testState_oneRow_withTabs() {
-        mockResources();
         when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(false);
 
 
@@ -378,7 +363,7 @@
         });
         mToolbar.setMenuItems(Collections.singletonList(item));
 
-        assertThat(getMenuItemView(0).isShown()).isTrue();
+        assertThat(getMenuItemView(0).getVisibility()).isEqualTo(View.VISIBLE);
     }
 
     @Test
@@ -388,7 +373,7 @@
         mToolbar.setMenuItems(Collections.singletonList(item));
 
         item.setVisible(false);
-        assertThat(getMenuItemView(0).isShown()).isFalse();
+        assertThat(getMenuItemView(0).getVisibility()).isNotEqualTo(View.VISIBLE);
     }
 
     @Test
@@ -399,7 +384,7 @@
 
         item.setVisible(false);
         item.setVisible(true);
-        assertThat(getMenuItemView(0).isShown()).isTrue();
+        assertThat(getMenuItemView(0).getVisibility()).isEqualTo(View.VISIBLE);
     }
 
     @Test
@@ -430,8 +415,8 @@
         mToolbar.setShowMenuItemsWhileSearching(false);
         mToolbar.setState(Toolbar.State.SEARCH);
 
-        assertThat(getMenuItemView(0).isShown()).isFalse();
-        assertThat(getMenuItemView(1).isShown()).isFalse();
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_menu_items_container).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
     }
 
     @Test
@@ -444,8 +429,10 @@
         mToolbar.setShowMenuItemsWhileSearching(true);
         mToolbar.setState(Toolbar.State.SEARCH);
 
-        assertThat(getMenuItemView(0).isShown()).isFalse();
-        assertThat(getMenuItemView(1).isShown()).isTrue();
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_menu_items_container).getVisibility())
+                .isEqualTo(View.VISIBLE);
+        assertThat(getMenuItemView(0).getVisibility()).isNotEqualTo(View.VISIBLE);
+        assertThat(getMenuItemView(1).getVisibility()).isEqualTo(View.VISIBLE);
     }
 
     private MenuItem createMenuItem(MenuItem.OnClickListener listener) {
@@ -455,12 +442,6 @@
                 .build();
     }
 
-    private void mockResources() {
-        mContext = spy(RuntimeEnvironment.application);
-        mResources = spy(mContext.getResources());
-        when(mContext.getResources()).thenReturn(mResources);
-    }
-
     private int getMenuItemCount() {
         return mToolbar.getMenuItems().size();
     }
diff --git a/connected-device-lib/Android.bp b/connected-device-lib/Android.bp
new file mode 100644
index 0000000..85490be
--- /dev/null
+++ b/connected-device-lib/Android.bp
@@ -0,0 +1,43 @@
+//
+// 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",
+
+    resource_dirs: ["res"],
+
+    optimize: {
+        enabled: false,
+    },
+
+    libs: ["android.car"],
+
+    static_libs: [
+        "EncryptionRunner-lib",
+        "androidx.room_room-runtime",
+        "connected-device-protos",
+    ],
+
+    plugins: [
+        "car-androidx-room-compiler",
+    ],
+
+    platform_apis: true,
+}
diff --git a/connected-device-lib/AndroidManifest.xml b/connected-device-lib/AndroidManifest.xml
new file mode 100644
index 0000000..d02ffce
--- /dev/null
+++ b/connected-device-lib/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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
new file mode 100644
index 0000000..108da4e
--- /dev/null
+++ b/connected-device-lib/OWNERS
@@ -0,0 +1,5 @@
+# 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/lib/kotlin-reflect-sources.jar b/connected-device-lib/lib/kotlin-reflect-sources.jar
new file mode 100644
index 0000000..917a722
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-reflect-sources.jar
Binary files differ
diff --git a/connected-device-lib/lib/kotlin-reflect.jar b/connected-device-lib/lib/kotlin-reflect.jar
new file mode 100644
index 0000000..e872351
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-reflect.jar
Binary files differ
diff --git a/connected-device-lib/lib/kotlin-stdlib-jdk7-sources.jar b/connected-device-lib/lib/kotlin-stdlib-jdk7-sources.jar
new file mode 100644
index 0000000..551568d
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-stdlib-jdk7-sources.jar
Binary files differ
diff --git a/connected-device-lib/lib/kotlin-stdlib-jdk7.jar b/connected-device-lib/lib/kotlin-stdlib-jdk7.jar
new file mode 100644
index 0000000..d80ae96
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-stdlib-jdk7.jar
Binary files differ
diff --git a/connected-device-lib/lib/kotlin-stdlib-jdk8-sources.jar b/connected-device-lib/lib/kotlin-stdlib-jdk8-sources.jar
new file mode 100644
index 0000000..3538660
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-stdlib-jdk8-sources.jar
Binary files differ
diff --git a/connected-device-lib/lib/kotlin-stdlib-jdk8.jar b/connected-device-lib/lib/kotlin-stdlib-jdk8.jar
new file mode 100644
index 0000000..08101a3
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-stdlib-jdk8.jar
Binary files differ
diff --git a/connected-device-lib/lib/kotlin-stdlib-sources.jar b/connected-device-lib/lib/kotlin-stdlib-sources.jar
new file mode 100644
index 0000000..2bdaf9e
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-stdlib-sources.jar
Binary files differ
diff --git a/connected-device-lib/lib/kotlin-stdlib.jar b/connected-device-lib/lib/kotlin-stdlib.jar
new file mode 100644
index 0000000..2bd7644
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-stdlib.jar
Binary files differ
diff --git a/connected-device-lib/lib/kotlin-test-sources.jar b/connected-device-lib/lib/kotlin-test-sources.jar
new file mode 100644
index 0000000..7bd21ce
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-test-sources.jar
Binary files differ
diff --git a/connected-device-lib/lib/kotlin-test.jar b/connected-device-lib/lib/kotlin-test.jar
new file mode 100644
index 0000000..ede1d8b
--- /dev/null
+++ b/connected-device-lib/lib/kotlin-test.jar
Binary files differ
diff --git a/connected-device-lib/proto/Android.bp b/connected-device-lib/proto/Android.bp
new file mode 100644
index 0000000..c9dcb73
--- /dev/null
+++ b/connected-device-lib/proto/Android.bp
@@ -0,0 +1,26 @@
+//
+// 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/ble_device_message.proto b/connected-device-lib/proto/ble_device_message.proto
new file mode 100644
index 0000000..581d6a0
--- /dev/null
+++ b/connected-device-lib/proto/ble_device_message.proto
@@ -0,0 +1,39 @@
+/*
+ * 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.BleStreamProtos";
+option java_outer_classname = "BleDeviceMessageProto";
+
+// A message between devices.
+message BleDeviceMessage {
+  // 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/ble_packet.proto b/connected-device-lib/proto/ble_packet.proto
new file mode 100644
index 0000000..c2ce262
--- /dev/null
+++ b/connected-device-lib/proto/ble_packet.proto
@@ -0,0 +1,38 @@
+/*
+ * 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.BleStreamProtos";
+option java_outer_classname = "BlePacketProto";
+
+// A packet across a BLE channel.
+message BlePacket {
+  // 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/ble_version_exchange.proto b/connected-device-lib/proto/ble_version_exchange.proto
new file mode 100644
index 0000000..a7e8021
--- /dev/null
+++ b/connected-device-lib/proto/ble_version_exchange.proto
@@ -0,0 +1,36 @@
+/*
+ * 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.BleStreamProtos";
+option java_outer_classname = "VersionExchangeProto";
+
+message BleVersionExchange {
+  // 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/proto/jarjar-rules.txt b/connected-device-lib/proto/jarjar-rules.txt
new file mode 100644
index 0000000..d27aecb
--- /dev/null
+++ b/connected-device-lib/proto/jarjar-rules.txt
@@ -0,0 +1 @@
+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
new file mode 100644
index 0000000..d447ccc
--- /dev/null
+++ b/connected-device-lib/proto/operation_type.proto
@@ -0,0 +1,44 @@
+/*
+ * 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.BleStreamProtos";
+option java_outer_classname = "BleOperationProto";
+
+// 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/res/values/config.xml b/connected-device-lib/res/values/config.xml
new file mode 100644
index 0000000..0f88ddc
--- /dev/null
+++ b/connected-device-lib/res/values/config.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <string name="car_service_uuid" translatable="false">5e2a68a8-27be-43f9-8d1e-4546976fabd7</string>
+    <string name="car_association_service_uuid" translatable="false">5e2a68a4-27be-43f9-8d1e-4546976fabd7</string>
+    <string name="car_bg_mask" translatable="false">00000000000000000000000000000000</string>
+
+    <string name="car_secure_read_uuid" translatable="false">5e2a68a6-27be-43f9-8d1e-4546976fabd7</string>
+    <string name="car_secure_write_uuid" translatable="false">5e2a68a5-27be-43f9-8d1e-4546976fabd7</string>
+
+    <string name="connected_device_shared_preferences" translatable="false">com.android.car.connecteddevice</string>
+
+    <integer name="car_reconnect_timeout_sec">60</integer>
+</resources>
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/AssociationCallback.java b/connected-device-lib/src/com/android/car/connecteddevice/AssociationCallback.java
new file mode 100644
index 0000000..fb7000b
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/AssociationCallback.java
@@ -0,0 +1,56 @@
+/*
+ * 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 android.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.
+     */
+    void onAssociationStartSuccess(@NonNull String deviceName);
+
+    /** Invoked when IHU failed to start advertising for association. */
+    void onAssociationStartFailure();
+
+    /**
+     * Invoked when a {@link ConnectedDeviceManager.DeviceError} has been encountered in attempting
+     * to associate a new device.
+     *
+     * @param error The failure indication.
+     */
+    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.
+     */
+    void onVerificationCodeAvailable(@NonNull String code);
+
+    /**
+     * Invoked when the association has completed.
+     *
+     * @param deviceId The id of the newly associated device.
+     */
+    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
new file mode 100644
index 0000000..f8805a1
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ConnectedDeviceManager.java
@@ -0,0 +1,934 @@
+/*
+ * 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 android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+
+import com.android.car.connecteddevice.ble.BleCentralManager;
+import com.android.car.connecteddevice.ble.BlePeripheralManager;
+import com.android.car.connecteddevice.ble.CarBleCentralManager;
+import com.android.car.connecteddevice.ble.CarBleManager;
+import com.android.car.connecteddevice.ble.CarBlePeripheralManager;
+import com.android.car.connecteddevice.ble.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 com.android.car.connecteddevice.util.EventLog;
+import com.android.car.connecteddevice.util.ThreadSafeCallbacks;
+import com.android.internal.annotations.VisibleForTesting;
+
+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 CarBleCentralManager mCentralManager;
+
+    private final CarBlePeripheralManager mPeripheralManager;
+
+    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, InternalConnectedDevice> mConnectedDevices =
+            new ConcurrentHashMap<>();
+
+    // recipientId -> (deviceId -> message bytes)
+    private final Map<UUID, Map<String, 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 final int mReconnectTimeoutSeconds;
+
+    private String mNameForAssociation;
+
+    private AssociationCallback mAssociationCallback;
+
+    private MessageDeliveryDelegate mMessageDeliveryDelegate;
+
+    @Retention(SOURCE)
+    @IntDef(prefix = { "DEVICE_ERROR_" },
+            value = {
+                    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 Context context) {
+        this(context, new ConnectedDeviceStorage(context), new BleCentralManager(context),
+                new BlePeripheralManager(context),
+                UUID.fromString(context.getString(R.string.car_service_uuid)),
+                UUID.fromString(context.getString(R.string.car_association_service_uuid)),
+                context.getString(R.string.car_bg_mask),
+                UUID.fromString(context.getString(R.string.car_secure_write_uuid)),
+                UUID.fromString(context.getString(R.string.car_secure_read_uuid)),
+                context.getResources().getInteger(R.integer.car_reconnect_timeout_sec));
+    }
+
+    private ConnectedDeviceManager(
+            @NonNull Context context,
+            @NonNull ConnectedDeviceStorage storage,
+            @NonNull BleCentralManager bleCentralManager,
+            @NonNull BlePeripheralManager blePeripheralManager,
+            @NonNull UUID serviceUuid,
+            @NonNull UUID associationServiceUuid,
+            @NonNull String bgMask,
+            @NonNull UUID writeCharacteristicUuid,
+            @NonNull UUID readCharacteristicUuid,
+            int reconnectTimeoutSeconds) {
+        this(storage,
+                new CarBleCentralManager(context, bleCentralManager, storage, serviceUuid, bgMask,
+                        writeCharacteristicUuid, readCharacteristicUuid),
+                new CarBlePeripheralManager(blePeripheralManager, storage, associationServiceUuid,
+                        writeCharacteristicUuid, readCharacteristicUuid), reconnectTimeoutSeconds);
+    }
+
+    @VisibleForTesting
+    ConnectedDeviceManager(
+            @NonNull ConnectedDeviceStorage storage,
+            @NonNull CarBleCentralManager centralManager,
+            @NonNull CarBlePeripheralManager peripheralManager,
+            int reconnectTimeoutSeconds) {
+        Executor callbackExecutor = Executors.newSingleThreadExecutor();
+        mStorage = storage;
+        mCentralManager = centralManager;
+        mPeripheralManager = peripheralManager;
+        mCentralManager.registerCallback(generateCarBleCallback(centralManager), callbackExecutor);
+        mPeripheralManager.registerCallback(generateCarBleCallback(peripheralManager),
+                callbackExecutor);
+        mStorage.setAssociatedDeviceCallback(mAssociatedDeviceCallback);
+        mReconnectTimeoutSeconds = reconnectTimeoutSeconds;
+    }
+
+    /**
+     * 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();
+        }
+        // TODO (b/141312136) Start central manager
+        mPeripheralManager.start();
+        connectToActiveUserDevice();
+    }
+
+    /** Reset internal processes and disconnect any active connections. */
+    public void reset() {
+        logd(TAG, "Resetting ConnectedDeviceManager.");
+        for (InternalConnectedDevice device : mConnectedDevices.values()) {
+            removeConnectedDevice(device.mConnectedDevice.getDeviceId(), device.mCarBleManager);
+        }
+        mPeripheralManager.stop();
+        // TODO (b/141312136) Stop central manager
+        mIsConnectingToUserDevice.set(false);
+    }
+
+    /** Returns {@link List<ConnectedDevice>} of devices currently connected. */
+    @NonNull
+    public List<ConnectedDevice> getActiveUserConnectedDevices() {
+        List<ConnectedDevice> activeUserConnectedDevices = new ArrayList<>();
+        for (InternalConnectedDevice device : mConnectedDevices.values()) {
+            if (device.mConnectedDevice.isAssociatedWithActiveUser()) {
+                activeUserConnectedDevices.add(device.mConnectedDevice);
+            }
+        }
+        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 @CallbackExecutor 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 @CallbackExecutor 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 {
+            if (mIsConnectingToUserDevice.get()) {
+                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.");
+                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 + ".");
+                return;
+            }
+            if (mConnectedDevices.containsKey(userDevice.getDeviceId())) {
+                logd(TAG, "Device has already been connected. No need to attempt connection "
+                        + "again.");
+                return;
+            }
+            EventLog.onStartDeviceSearchStarted();
+            mIsConnectingToUserDevice.set(true);
+            mPeripheralManager.connectToDevice(UUID.fromString(userDevice.getDeviceId()),
+                    mReconnectTimeoutSeconds);
+        } 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.");
+            mPeripheralManager.startAssociation(getNameForAssociation(),
+                    mInternalAssociationCallback);
+        }).start();
+    }
+
+    /** 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;
+        }
+        mAssociationCallback = null;
+        mPeripheralManager.stopAssociation(mInternalAssociationCallback);
+    }
+
+    /**
+     * 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() {
+        mPeripheralManager.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) {
+        InternalConnectedDevice device = mConnectedDevices.get(deviceId);
+        if (device != null) {
+            device.mCarBleManager.disconnectDevice(deviceId);
+            removeConnectedDevice(deviceId, device.mCarBleManager);
+        }
+    }
+
+    /**
+     * 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 @CallbackExecutor 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 HashMap<>());
+
+        // 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);
+
+        byte[] message = popMissedMessage(recipientId, device.getDeviceId());
+        if (message != null) {
+            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.putIfAbsent(recipientId, new HashMap<>());
+        mRecipientMissedMessages.get(recipientId).putIfAbsent(deviceId, 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 last missed {@code byte[]} of the message, or {@code null} if no messages were
+     *         missed.
+     */
+    @Nullable
+    private byte[] popMissedMessage(@NonNull UUID recipientId, @NonNull String deviceId) {
+        Map<String, 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 + ".");
+
+        InternalConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
+        if (connectedDevice == null) {
+            loge(TAG, "Attempted to send message to unknown device " + deviceId + ". Ignoring.");
+            return;
+        }
+
+        if (isEncrypted && !connectedDevice.mConnectedDevice.hasSecureChannel()) {
+            throw new IllegalStateException("Cannot send a message securely to device that has not "
+                    + "established a secure channel.");
+        }
+
+        connectedDevice.mCarBleManager.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;
+        }
+
+        InternalConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
+        if (connectedDevice != null) {
+            recipientCallbacks.get(recipientId).invoke(
+                    callback ->
+                            callback.onDeviceError(connectedDevice.mConnectedDevice,
+                                    DEVICE_ERROR_INSECURE_RECIPIENT_ID_DETECTED)
+            );
+        }
+
+        recipientCallbacks.remove(recipientId);
+        mBlacklistedRecipients.add(recipientId);
+    }
+
+    @VisibleForTesting
+    void addConnectedDevice(@NonNull String deviceId, @NonNull CarBleManager bleManager) {
+        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, new InternalConnectedDevice(connectedDevice, bleManager));
+        invokeConnectionCallbacks(connectedDevice.isAssociatedWithActiveUser(),
+                callback -> callback.onDeviceConnected(connectedDevice));
+    }
+
+    @VisibleForTesting
+    void removeConnectedDevice(@NonNull String deviceId, @NonNull CarBleManager bleManager) {
+        logd(TAG, "Device " + deviceId + " disconnected from manager " + bleManager);
+        InternalConnectedDevice connectedDevice = getConnectedDeviceForManager(deviceId,
+                bleManager);
+
+        // If disconnect happened on peripheral, open for future requests to connect.
+        if (bleManager == mPeripheralManager) {
+            mIsConnectingToUserDevice.set(false);
+        }
+
+        if (connectedDevice == null) {
+            return;
+        }
+
+        mConnectedDevices.remove(deviceId);
+        boolean isAssociated = connectedDevice.mConnectedDevice.isAssociatedWithActiveUser();
+        invokeConnectionCallbacks(isAssociated,
+                callback -> callback.onDeviceDisconnected(connectedDevice.mConnectedDevice));
+
+        if (isAssociated || mConnectedDevices.isEmpty()) {
+            // Try to regain connection to active user's device.
+            connectToActiveUserDevice();
+        }
+    }
+
+    @VisibleForTesting
+    void onSecureChannelEstablished(@NonNull String deviceId,
+            @NonNull CarBleManager bleManager) {
+        if (mConnectedDevices.get(deviceId) == null) {
+            loge(TAG, "Secure channel established on unknown device " + deviceId + ".");
+            return;
+        }
+        ConnectedDevice connectedDevice = mConnectedDevices.get(deviceId).mConnectedDevice;
+        ConnectedDevice updatedConnectedDevice = new ConnectedDevice(connectedDevice.getDeviceId(),
+                connectedDevice.getDeviceName(), connectedDevice.isAssociatedWithActiveUser(),
+                /* hasSecureChannel = */ true);
+
+        boolean notifyCallbacks = getConnectedDeviceForManager(deviceId, bleManager) != null;
+
+        // TODO (b/143088482) Implement interrupt
+        // Ignore if central already holds the active device connection and interrupt the
+        // connection.
+
+        mConnectedDevices.put(deviceId,
+                new InternalConnectedDevice(updatedConnectedDevice, bleManager));
+        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.");
+
+        InternalConnectedDevice 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.mConnectedDevice)) {
+            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.mConnectedDevice,
+                        message.getMessage()));
+    }
+
+    @VisibleForTesting
+    void deviceErrorOccurred(@NonNull String deviceId) {
+        InternalConnectedDevice 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.mConnectedDevice,
+                        DEVICE_ERROR_INVALID_SECURITY_KEY));
+    }
+
+    @VisibleForTesting
+    void onAssociationCompleted(@NonNull String deviceId) {
+        InternalConnectedDevice connectedDevice =
+                getConnectedDeviceForManager(deviceId, mPeripheralManager);
+        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.mConnectedDevice.isAssociatedWithActiveUser()) {
+            // Device was already marked as belonging to active user. No need to reissue callbacks.
+            return;
+        }
+        removeConnectedDevice(deviceId, mPeripheralManager);
+        addConnectedDevice(deviceId, mPeripheralManager);
+    }
+
+    @NonNull
+    private List<String> getActiveUserDeviceIds() {
+        return mStorage.getActiveUserAssociatedDeviceIds();
+    }
+
+    @Nullable
+    private InternalConnectedDevice getConnectedDeviceForManager(@NonNull String deviceId,
+            @NonNull CarBleManager bleManager) {
+        InternalConnectedDevice connectedDevice = mConnectedDevices.get(deviceId);
+        if (connectedDevice != null && connectedDevice.mCarBleManager == bleManager) {
+            return connectedDevice;
+        }
+
+        return null;
+    }
+
+    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 CarBleManager.Callback generateCarBleCallback(@NonNull CarBleManager carBleManager) {
+        return new CarBleManager.Callback() {
+            @Override
+            public void onDeviceConnected(String deviceId) {
+                EventLog.onDeviceIdReceived();
+                addConnectedDevice(deviceId, carBleManager);
+            }
+
+            @Override
+            public void onDeviceDisconnected(String deviceId) {
+                removeConnectedDevice(deviceId, carBleManager);
+            }
+
+            @Override
+            public void onSecureChannelEstablished(String deviceId) {
+                EventLog.onSecureChannelEstablished();
+                ConnectedDeviceManager.this.onSecureChannelEstablished(deviceId, carBleManager);
+            }
+
+            @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);
+    }
+
+    private static class InternalConnectedDevice {
+        private final ConnectedDevice mConnectedDevice;
+        private final CarBleManager mCarBleManager;
+
+        InternalConnectedDevice(@NonNull ConnectedDevice connectedDevice,
+                @NonNull CarBleManager carBleManager) {
+            mConnectedDevice = connectedDevice;
+            mCarBleManager = carBleManager;
+        }
+    }
+}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ble/BleCentralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/BleCentralManager.java
new file mode 100644
index 0000000..ca83a05
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/BleCentralManager.java
@@ -0,0 +1,194 @@
+/*
+ * 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.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.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+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 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/ble/BleDeviceMessageStream.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/BleDeviceMessageStream.java
new file mode 100644
index 0000000..f91693b
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/BleDeviceMessageStream.java
@@ -0,0 +1,384 @@
+/*
+ * 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.ble;
+
+import static com.android.car.connecteddevice.BleStreamProtos.BleOperationProto.OperationType;
+import static com.android.car.connecteddevice.BleStreamProtos.BlePacketProto.BlePacket;
+import static com.android.car.connecteddevice.BleStreamProtos.VersionExchangeProto.BleVersionExchange;
+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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.car.connecteddevice.BleStreamProtos.BleDeviceMessageProto.BleDeviceMessage;
+import com.android.car.connecteddevice.util.ByteUtils;
+import com.android.car.protobuf.ByteString;
+import com.android.car.protobuf.InvalidProtocolBufferException;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** BLE message stream to a device. */
+class BleDeviceMessageStream {
+
+    private static final String TAG = "BleDeviceMessageStream";
+
+    // Only version 2 of the messaging and version 1 of the security supported.
+    private static final int MESSAGING_VERSION = 2;
+    private static final int SECURITY_VERSION = 1;
+
+    /*
+     * 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 ArrayDeque<BlePacket> mPacketQueue = new ArrayDeque<>();
+
+    private final HashMap<Integer, ByteArrayOutputStream> mPendingData =
+            new HashMap<>();
+
+    private final MessageIdGenerator mMessageIdGenerator = new MessageIdGenerator();
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    private final AtomicBoolean mIsVersionExchanged = new AtomicBoolean(false);
+
+    private final AtomicBoolean mIsSendingInProgress = new AtomicBoolean(false);
+
+    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;
+
+    private MessageReceivedListener mMessageReceivedListener;
+
+    private MessageReceivedErrorListener mMessageReceivedErrorListener;
+
+    /*
+     * This initial value is 20 because BLE has a default write of 23 bytes. However, 3 bytes are
+     * subtracted due to bytes being reserved for the command type and attribute ID.
+     */
+    private int mMaxWriteSize = 20;
+
+    BleDeviceMessageStream(@NonNull BlePeripheralManager blePeripheralManager,
+            @NonNull BluetoothDevice device,
+            @NonNull BluetoothGattCharacteristic writeCharacteristic,
+            @NonNull BluetoothGattCharacteristic readCharacteristic) {
+        mBlePeripheralManager = blePeripheralManager;
+        mDevice = device;
+        mWriteCharacteristic = writeCharacteristic;
+        mReadCharacteristic = readCharacteristic;
+        mBlePeripheralManager.addOnCharacteristicWriteListener(this::onCharacteristicWrite);
+        mBlePeripheralManager.addOnCharacteristicReadListener(this::onCharacteristicRead);
+    }
+
+    /**
+     * 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.
+     */
+    void writeMessage(@NonNull DeviceMessage deviceMessage) {
+        writeMessage(deviceMessage, OperationType.CLIENT_MESSAGE);
+    }
+
+    /**
+     * Writes the given message to the write characteristic of this stream.
+     *
+     * This method will handle the chunking of messages based on the max write size. If it is
+     * a handshake message, the message recipient should be {@code null} and it cannot be
+     * encrypted.
+     *
+     * @param deviceMessage The data object contains recipient, isPayloadEncrypted and message.
+     * @param operationType The {@link OperationType} of this message.
+     */
+    void writeMessage(@NonNull DeviceMessage deviceMessage, OperationType operationType) {
+        logd(TAG, "Writing message to device: " + mDevice.getAddress() + ".");
+        BleDeviceMessage.Builder builder = BleDeviceMessage.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)));
+        }
+
+        BleDeviceMessage bleDeviceMessage = builder.build();
+        byte[] rawBytes = bleDeviceMessage.toByteArray();
+        List<BlePacket> blePackets;
+        try {
+            blePackets = BlePacketFactory.makeBlePackets(rawBytes, mMessageIdGenerator.next(),
+                    mMaxWriteSize);
+        } catch (BlePacketFactoryException e) {
+            loge(TAG, "Error while creating message packets.", e);
+            return;
+        }
+        mPacketQueue.addAll(blePackets);
+        writeNextMessageInQueue();
+    }
+
+    private void writeNextMessageInQueue() {
+        mHandler.postDelayed(() -> {
+            if (mPacketQueue.isEmpty()) {
+                logd(TAG, "No more packets to send.");
+                return;
+            }
+            if (mIsSendingInProgress.get()) {
+                logd(TAG, "Unable to send packet at this time.");
+                return;
+            }
+
+            mIsSendingInProgress.set(true);
+            BlePacket packet = mPacketQueue.remove();
+            logd(TAG, "Writing packet " + packet.getPacketNumber() + " of "
+                    + packet.getTotalPackets() + " for " + packet.getMessageId() + ".");
+            mWriteCharacteristic.setValue(packet.toByteArray());
+            mBlePeripheralManager.notifyCharacteristicChanged(mDevice, mWriteCharacteristic,
+                    /* confirm = */ false);
+        }, mThrottleDelay.get());
+    }
+
+    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.");
+        mIsSendingInProgress.set(false);
+        writeNextMessageInQueue();
+    }
+
+    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;
+        }
+
+        if (!mIsVersionExchanged.get()) {
+            processVersionExchange(device, value);
+            return;
+        }
+
+        BlePacket packet;
+        try {
+            packet = BlePacket.parseFrom(value);
+        } catch (InvalidProtocolBufferException e) {
+            loge(TAG, "Can not parse Ble packet from client.", e);
+            if (mMessageReceivedErrorListener != null) {
+                mMessageReceivedErrorListener.onMessageReceivedError(e);
+            }
+            return;
+        }
+        processPacket(packet);
+    }
+
+    private void processVersionExchange(@NonNull BluetoothDevice device, @NonNull byte[] value) {
+        BleVersionExchange versionExchange;
+        try {
+            versionExchange = BleVersionExchange.parseFrom(value);
+        } catch (InvalidProtocolBufferException e) {
+            loge(TAG, "Could not parse version exchange message", e);
+            if (mMessageReceivedErrorListener != null) {
+                mMessageReceivedErrorListener.onMessageReceivedError(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 + ".");
+            if (mMessageReceivedErrorListener != null) {
+                mMessageReceivedErrorListener.onMessageReceivedError(
+                        new IllegalStateException("Unsupported version."));
+            }
+            return;
+        }
+
+        BleVersionExchange headunitVersion = BleVersionExchange.newBuilder()
+                .setMinSupportedMessagingVersion(MESSAGING_VERSION)
+                .setMaxSupportedMessagingVersion(MESSAGING_VERSION)
+                .setMinSupportedSecurityVersion(SECURITY_VERSION)
+                .setMaxSupportedSecurityVersion(SECURITY_VERSION)
+                .build();
+        mWriteCharacteristic.setValue(headunitVersion.toByteArray());
+        mBlePeripheralManager.notifyCharacteristicChanged(device, mWriteCharacteristic,
+                /* confirm = */ false);
+        mIsVersionExchanged.set(true);
+        logd(TAG, "Sent supported version to the phone.");
+    }
+
+    @VisibleForTesting
+    void processPacket(@NonNull BlePacket packet) {
+        // Messages are coming in. Need to throttle outgoing messages to allow outgoing
+        // notifications to make it to the device.
+        mThrottleDelay.set(THROTTLE_WAIT_MS);
+
+        int messageId = packet.getMessageId();
+        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);
+            if (mMessageReceivedErrorListener != null) {
+                mMessageReceivedErrorListener.onMessageReceivedError(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);
+
+        // All message packets received. Resetting throttle back to default until next message
+        // started.
+        mThrottleDelay.set(THROTTLE_DEFAULT_MS);
+
+        logd(TAG, "Received complete device message " + messageId + " of " + messageBytes.length
+                + " bytes.");
+        BleDeviceMessage message;
+        try {
+            message = BleDeviceMessage.parseFrom(messageBytes);
+        } catch (InvalidProtocolBufferException e) {
+            loge(TAG, "Cannot parse device message from client.", e);
+            if (mMessageReceivedErrorListener != null) {
+                mMessageReceivedErrorListener.onMessageReceivedError(e);
+            }
+            return;
+        }
+
+        DeviceMessage deviceMessage = new DeviceMessage(
+                ByteUtils.bytesToUUID(message.getRecipient().toByteArray()),
+                message.getIsPayloadEncrypted(), message.getPayload().toByteArray());
+        if (mMessageReceivedListener != null) {
+            mMessageReceivedListener.onMessageReceived(deviceMessage, message.getOperation());
+        }
+    }
+
+    /** The maximum amount of bytes that can be written over BLE. */
+    void setMaxWriteSize(int maxWriteSize) {
+        mMaxWriteSize = maxWriteSize;
+    }
+
+    /**
+     * Set the given listener to be notified when a new message was received from the
+     * client. If listener is {@code null}, clear.
+     */
+    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.
+     */
+    void setMessageReceivedErrorListener(
+            @Nullable MessageReceivedErrorListener listener) {
+        mMessageReceivedErrorListener = listener;
+    }
+
+    /**
+     * Listener to be invoked when a complete message is received from the client.
+     */
+    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.
+     */
+    interface MessageReceivedErrorListener {
+        /**
+         * Called when there was an error during receiving message from the client.
+         *
+         * @param exception The error.
+         */
+        void onMessageReceivedError(@NonNull Exception exception);
+    }
+
+    /** 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;
+        }
+    }
+}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ble/BlePacketFactory.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/BlePacketFactory.java
new file mode 100644
index 0000000..a0d0bb1
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/BlePacketFactory.java
@@ -0,0 +1,155 @@
+/*
+ * 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.ble;
+
+import static com.android.car.connecteddevice.util.SafeLog.loge;
+
+import com.android.car.connecteddevice.BleStreamProtos.BlePacketProto.BlePacket;
+import com.android.car.protobuf.ByteString;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Factory for creating {@link BlePacket} protos.
+ */
+class BlePacketFactory {
+    private static final String TAG = "BlePacketFactory";
+
+    /**
+     * 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 BlePacket} 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 BlePacket}s.
+     * @param messageId The unique id for identifying message.
+     * @param maxSize The maximum size of each chunk.
+     * @return A list of {@link BlePacket}s.
+     * @throws BlePacketFactoryException if an error occurred during the splitting of data.
+     */
+    static List<BlePacket> makeBlePackets(byte[] payload, int messageId, int maxSize)
+            throws BlePacketFactoryException {
+        List<BlePacket> 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(BlePacket.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 BlePacket} 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 BlePacketFactoryException {
+        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 BlePacketFactoryException("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 BlePacketFactoryException("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 BlePacketFactory() {}
+}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ble/BlePacketFactoryException.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/BlePacketFactoryException.java
new file mode 100644
index 0000000..690ce28
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/BlePacketFactoryException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.ble;
+
+/**
+ * Exception for signaling {@link BlePacketFactory} errors.
+ */
+class BlePacketFactoryException extends Exception {
+    BlePacketFactoryException(String message) {
+        super(message);
+    }
+}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ble/BlePeripheralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/BlePeripheralManager.java
new file mode 100644
index 0000000..6d50f63
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/BlePeripheralManager.java
@@ -0,0 +1,520 @@
+/*
+ * 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.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.annotation.NonNull;
+import android.annotation.Nullable;
+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 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 BluetoothLeAdvertiser mAdvertiser;
+    private int mAdvertiserStartCount;
+    private int mGattServerRetryStartCount;
+    private BluetoothGattService mBluetoothGattService;
+    private AdvertiseCallback mAdvertiseCallback;
+    private AdvertiseData mAdvertiseData;
+
+    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 advertiseCallback {@link AdvertiseCallback} callback for advertiser
+     */
+    void startAdvertising(
+            BluetoothGattService service, AdvertiseData data, AdvertiseCallback advertiseCallback) {
+        logd(TAG, "startAdvertising: " + 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;
+        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) {
+        if (mAdvertiser != null) {
+            logd(TAG, "Stop advertising.");
+            mAdvertiser.stopAdvertising(advertiseCallback);
+        }
+    }
+
+    /**
+     * 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() {
+        // Stops the advertiser, scanner and GATT server. This needs to be done to avoid leaks.
+        if (mAdvertiser != null) {
+            mAdvertiser.stopAdvertising(mAdvertiseCallback);
+        }
+        // Clears all registered listeners. IHU only supports single connection in peripheral role.
+        mReadListeners.clear();
+        mWriteListeners.clear();
+        mAdvertiser = null;
+
+        BluetoothGattServer gattServer = mGattServer.getAndSet(null);
+        if (gattServer == null) {
+            return;
+        }
+
+        logd(TAG, "stopGattServer");
+        BluetoothGatt bluetoothGatt = mBluetoothGatt.getAndSet(null);
+        if (bluetoothGatt != null) {
+            gattServer.cancelConnection(bluetoothGatt.getDevice());
+            bluetoothGatt.disconnect();
+        }
+        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, 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 data, AdvertiseCallback advertiseCallback) {
+        if (BluetoothAdapter.getDefaultAdapter() != null) {
+            mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
+        }
+
+        if (mAdvertiser != null) {
+            logd(TAG, "Advertiser created, retry count: " + mAdvertiserStartCount);
+            mAdvertiser.startAdvertising(settings, data, advertiseCallback);
+            mAdvertiserStartCount = 0;
+        } else if (mAdvertiserStartCount < BLE_RETRY_LIMIT) {
+            mHandler.postDelayed(
+                    () -> startAdvertisingInternally(settings, data, 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) {
+                    logd(TAG, "BLE Connection State Change: " + newState);
+                    switch (newState) {
+                        case BluetoothProfile.STATE_CONNECTED:
+                            for (Callback callback : mCallbacks) {
+                                callback.onRemoteDeviceConnected(device);
+                            }
+                            break;
+                        case BluetoothProfile.STATE_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/ble/CarBleCentralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBleCentralManager.java
new file mode 100644
index 0000000..a9168a8
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBleCentralManager.java
@@ -0,0 +1,345 @@
+/*
+ * 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.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.annotation.NonNull;
+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 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 CarBleManager {
+
+    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<BleDevice> 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 + ".");
+        BleDevice device = getConnectedDevice(deviceId);
+        if (device == null) {
+            return;
+        }
+
+        deviceDisconnected(device, STATUS_FORCED_DISCONNECT);
+    }
+
+    private void ignoreDevice(@NonNull BleDevice device) {
+        mIgnoredDevices.add(device);
+    }
+
+    private boolean isDeviceIgnored(@NonNull BluetoothDevice device) {
+        for (BleDevice bleDevice : mIgnoredDevices) {
+            if (device.equals(bleDevice.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;
+        }
+
+        BleDevice bleDevice = new BleDevice(device, gatt);
+        bleDevice.mState = BleDeviceState.CONNECTING;
+        addConnectedDevice(bleDevice);
+
+        // Stop scanning if we have reached the maximum number of connections.
+        if (getConnectedDevicesCount() >= MAX_CONNECTIONS) {
+            mBleCentralManager.stopScanning();
+        }
+    }
+
+    private void deviceConnected(@NonNull BleDevice device) {
+        if (device.mGatt == null) {
+            loge(TAG, "Device connected with null gatt. Disconnecting.");
+            deviceDisconnected(device, BluetoothProfile.STATE_DISCONNECTED);
+            return;
+        }
+        device.mState = BleDeviceState.PENDING_VERIFICATION;
+        device.mGatt.discoverServices();
+        logd(TAG, "New device connected: " + device.mGatt.getDevice().getAddress()
+                + ". Active connections: " + getConnectedDevicesCount() + ".");
+    }
+
+    private void deviceDisconnected(@NonNull BleDevice 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;
+            }
+
+            BleDevice 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;
+            }
+
+            BleDevice connectedDevice = getConnectedDevice(gatt);
+            if (connectedDevice == null) {
+                return;
+            }
+            BluetoothGattService service = gatt.getService(mServiceUuid);
+            if (service == null) {
+                ignoreDevice(connectedDevice);
+                gatt.disconnect();
+                return;
+            }
+
+            connectedDevice.mState = BleDeviceState.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/ble/CarBleManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBleManager.java
new file mode 100644
index 0000000..0b05906
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBleManager.java
@@ -0,0 +1,253 @@
+/*
+ * 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.ble;
+
+import static com.android.car.connecteddevice.util.SafeLog.logd;
+import static com.android.car.connecteddevice.util.SafeLog.logw;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+
+import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
+import com.android.car.connecteddevice.util.ThreadSafeCallbacks;
+
+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 CarBleManager {
+
+    private static final String TAG = "CarBleManager";
+
+    final ConnectedDeviceStorage mStorage;
+
+    final CopyOnWriteArraySet<BleDevice> mConnectedDevices = new CopyOnWriteArraySet<>();
+
+    final ThreadSafeCallbacks<Callback> mCallbacks = new ThreadSafeCallbacks<>();
+
+    protected CarBleManager(@NonNull ConnectedDeviceStorage connectedDeviceStorage) {
+        mStorage = connectedDeviceStorage;
+    }
+
+    /**
+     * Initialize and start the manager.
+     */
+    public void start() {
+    }
+
+    /**
+     * Stop the manager and clean up.
+     */
+    public void stop() {
+        for (BleDevice device : mConnectedDevices) {
+            if (device.mGatt != null) {
+                device.mGatt.close();
+            }
+        }
+        mConnectedDevices.clear();
+    }
+
+    /**
+     * 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) {
+        BleDevice 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 BleDevice}.
+     * @param message {@link DeviceMessage} to send.
+     */
+    public void sendMessage(@NonNull BleDevice device, @NonNull DeviceMessage message) {
+        String deviceId = device.mDeviceId;
+        if (deviceId == null) {
+            deviceId = "Unidentified device";
+        }
+
+        logd(TAG, "Writing " + message.getMessage().length + " bytes to " + deviceId + ".");
+
+
+        if (message.isMessageEncrypted()) {
+            device.mSecureChannel.sendEncryptedMessage(message);
+        } else {
+            device.mSecureChannel.getStream().writeMessage(message);
+        }
+    }
+
+    /**
+     * Get the {@link BleDevice} with matching {@link BluetoothGatt} if available. Returns
+     * {@code null} if no matches are found.
+     */
+    @Nullable
+    BleDevice getConnectedDevice(@NonNull BluetoothGatt gatt) {
+        for (BleDevice device : mConnectedDevices) {
+            if (device.mGatt == gatt) {
+                return device;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Get the {@link BleDevice} with matching {@link BluetoothDevice} if available. Returns
+     * {@code null} if no matches are found.
+     */
+    @Nullable
+    BleDevice getConnectedDevice(@NonNull BluetoothDevice device) {
+        for (BleDevice connectedDevice : mConnectedDevices) {
+            if (device.equals(connectedDevice.mDevice)) {
+                return connectedDevice;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Get the {@link BleDevice} with matching device id if available. Returns {@code null} if
+     * no matches are found.
+     */
+    @Nullable
+    BleDevice getConnectedDevice(@NonNull String deviceId) {
+        for (BleDevice device : mConnectedDevices) {
+            if (deviceId.equals(device.mDeviceId)) {
+                return device;
+            }
+        }
+
+        return null;
+    }
+
+    /** Add the {@link BleDevice} that has connected. */
+    void addConnectedDevice(@NonNull BleDevice device) {
+        mConnectedDevices.add(device);
+    }
+
+    /** Return the number of devices currently connected. */
+    int getConnectedDevicesCount() {
+        return mConnectedDevices.size();
+    }
+
+    /** Remove [@link BleDevice} that has been disconnected. */
+    void removeConnectedDevice(@NonNull BleDevice device) {
+        mConnectedDevices.remove(device);
+    }
+
+    /** Disconnect the provided device from this manager. */
+    public abstract void disconnectDevice(@NonNull String deviceId);
+
+    /** State for a connected device. */
+    enum BleDeviceState {
+        CONNECTING,
+        PENDING_VERIFICATION,
+        CONNECTED,
+        UNKNOWN
+    }
+
+    /**
+     * Container class to hold information about a connected device.
+     */
+    static class BleDevice {
+
+        BluetoothDevice mDevice;
+        BluetoothGatt mGatt;
+        BleDeviceState mState;
+        String mDeviceId;
+        SecureBleChannel mSecureChannel;
+
+        BleDevice(@NonNull BluetoothDevice device, @Nullable BluetoothGatt gatt) {
+            mDevice = device;
+            mGatt = gatt;
+            mState = BleDeviceState.UNKNOWN;
+        }
+    }
+
+    /**
+     * Callback for triggered events from {@link CarBleManager}.
+     */
+    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/ble/CarBlePeripheralManager.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java
new file mode 100644
index 0000000..6f279dd
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/CarBlePeripheralManager.java
@@ -0,0 +1,574 @@
+/*
+ * 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.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.annotation.NonNull;
+import android.annotation.Nullable;
+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.car.encryptionrunner.EncryptionRunnerFactory;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelUuid;
+
+import com.android.car.connecteddevice.AssociationCallback;
+import com.android.car.connecteddevice.model.AssociatedDevice;
+import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
+import com.android.car.connecteddevice.util.EventLog;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Communication manager that allows for targeted connections to a specific device in the car.
+ */
+public class CarBlePeripheralManager extends CarBleManager {
+
+    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;
+
+    // Arbitrary delay time for a retry of association advertising if bluetooth adapter name change
+    // fails.
+    private static final long ASSOCIATE_ADVERTISING_DELAY_MS = 10L;
+
+    private static final UUID CLIENT_CHARACTERISTIC_CONFIG =
+            UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
+
+    private final BluetoothGattDescriptor mDescriptor =
+            new BluetoothGattDescriptor(CLIENT_CHARACTERISTIC_CONFIG,
+                    BluetoothGattDescriptor.PERMISSION_READ
+                            | BluetoothGattDescriptor.PERMISSION_WRITE);
+
+    private final ScheduledExecutorService mScheduler =
+            Executors.newSingleThreadScheduledExecutor();
+
+    private final BlePeripheralManager mBlePeripheralManager;
+
+    private final UUID mAssociationServiceUuid;
+
+    private final BluetoothGattCharacteristic mWriteCharacteristic;
+
+    private final BluetoothGattCharacteristic mReadCharacteristic;
+
+    private final Handler mTimeoutHandler;
+
+    // BLE default is 23, minus 3 bytes for ATT_PROTOCOL.
+    private int mWriteSize = 20;
+
+    private String mOriginalBluetoothName;
+
+    private String mClientDeviceName;
+
+    private String mClientDeviceAddress;
+
+    private AssociationCallback mAssociationCallback;
+
+    private AdvertiseCallback mAdvertiseCallback;
+
+    /**
+     * 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 writeCharacteristicUuid {@link UUID} of characteristic the car will write to.
+     * @param readCharacteristicUuid {@link UUID} of characteristic the device will write to.
+     */
+    public CarBlePeripheralManager(@NonNull BlePeripheralManager blePeripheralManager,
+            @NonNull ConnectedDeviceStorage connectedDeviceStorage,
+            @NonNull UUID associationServiceUuid, @NonNull UUID writeCharacteristicUuid,
+            @NonNull UUID readCharacteristicUuid) {
+        super(connectedDeviceStorage);
+        mBlePeripheralManager = blePeripheralManager;
+        mAssociationServiceUuid = associationServiceUuid;
+        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);
+        mTimeoutHandler = new Handler(Looper.getMainLooper());
+    }
+
+    @Override
+    public void start() {
+        super.start();
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        if (adapter == null) {
+            return;
+        }
+        String originalBluetoothName = mStorage.getStoredBluetoothName();
+        if (originalBluetoothName == null) {
+            return;
+        }
+        if (originalBluetoothName.equals(adapter.getName())) {
+            mStorage.removeStoredBluetoothName();
+            return;
+        }
+
+        logw(TAG, "Discovered mismatch in bluetooth adapter name. Resetting back to "
+                + originalBluetoothName + ".");
+        adapter.setName(originalBluetoothName);
+        mScheduler.schedule(
+                () -> verifyBluetoothNameRestored(originalBluetoothName),
+                ASSOCIATE_ADVERTISING_DELAY_MS, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+        reset();
+    }
+
+    @Override
+    public void disconnectDevice(@NonNull String deviceId) {
+        BleDevice connectedDevice = getConnectedDevice();
+        if (connectedDevice == null || !deviceId.equals(connectedDevice.mDeviceId)) {
+            return;
+        }
+        reset();
+    }
+
+    private void reset() {
+        resetBluetoothAdapterName();
+        mClientDeviceAddress = null;
+        mClientDeviceName = null;
+        mAssociationCallback = null;
+        mBlePeripheralManager.cleanup();
+        mConnectedDevices.clear();
+    }
+
+    /** Attempt to connect to device with provided id within set timeout period. */
+    public void connectToDevice(@NonNull UUID deviceId, int timeoutSeconds) {
+        for (BleDevice 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();
+
+        mAdvertiseCallback = new AdvertiseCallback() {
+            @Override
+            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+                super.onStartSuccess(settingsInEffect);
+                mTimeoutHandler.postDelayed(mTimeoutRunnable,
+                        TimeUnit.SECONDS.toMillis(timeoutSeconds));
+                logd(TAG, "Successfully started advertising for device " + deviceId
+                        + " for " + timeoutSeconds + " seconds.");
+            }
+        };
+        mBlePeripheralManager.unregisterCallback(mAssociationPeripheralCallback);
+        mBlePeripheralManager.registerCallback(mReconnectPeripheralCallback);
+        mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
+        startAdvertising(deviceId, mAdvertiseCallback, /* includeDeviceName = */ false);
+    }
+
+    @Nullable
+    private BleDevice getConnectedDevice() {
+        if (mConnectedDevices.isEmpty()) {
+            return null;
+        }
+        return mConnectedDevices.iterator().next();
+    }
+
+    /** Start the association with a new device */
+    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;
+        if (mOriginalBluetoothName == null) {
+            mOriginalBluetoothName = adapter.getName();
+            mStorage.storeBluetoothName(mOriginalBluetoothName);
+        }
+        adapter.setName(nameForAssociation);
+        logd(TAG, "Changing bluetooth adapter name from " + mOriginalBluetoothName + " to "
+                + nameForAssociation + ".");
+        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);
+            }
+        };
+        attemptAssociationAdvertising(nameForAssociation, callback);
+    }
+
+    /** Stop the association with any device. */
+    public void stopAssociation(@NonNull AssociationCallback callback) {
+        if (!isAssociating() || callback != mAssociationCallback) {
+            return;
+        }
+        reset();
+    }
+
+    private void attemptAssociationAdvertising(@NonNull String adapterName,
+            @NonNull AssociationCallback callback) {
+        if (mOriginalBluetoothName != null
+                && adapterName.equals(BluetoothAdapter.getDefaultAdapter().getName())) {
+            startAdvertising(mAssociationServiceUuid, mAdvertiseCallback,
+                    /* includeDeviceName = */ true);
+            return;
+        }
+
+        ScheduledFuture future = mScheduler.schedule(
+                () -> attemptAssociationAdvertising(adapterName, callback),
+                ASSOCIATE_ADVERTISING_DELAY_MS, TimeUnit.MILLISECONDS);
+        if (future.isCancelled()) {
+            // Association failed to start.
+            callback.onAssociationStartFailure();
+            return;
+        }
+        logd(TAG, "Adapter name change has not taken affect prior to advertising attempt. Trying "
+                + "again in " + ASSOCIATE_ADVERTISING_DELAY_MS + "  milliseconds.");
+    }
+
+    private void startAdvertising(@NonNull UUID serviceUuid, @NonNull AdvertiseCallback callback,
+            boolean includeDeviceName) {
+        BluetoothGattService gattService = new BluetoothGattService(serviceUuid,
+                BluetoothGattService.SERVICE_TYPE_PRIMARY);
+        gattService.addCharacteristic(mWriteCharacteristic);
+        gattService.addCharacteristic(mReadCharacteristic);
+
+        AdvertiseData advertiseData = new AdvertiseData.Builder()
+                .setIncludeDeviceName(includeDeviceName)
+                .addServiceUuid(new ParcelUuid(serviceUuid))
+                .build();
+        mBlePeripheralManager.startAdvertising(gattService, advertiseData, callback);
+    }
+
+    /** 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;
+        }
+
+        SecureBleChannel secureChannel = getConnectedDevice().mSecureChannel;
+        if (secureChannel == null) {
+            disconnectWithError("Null SecureBleChannel found for the current connected device "
+                    + "when out-of-band confirmation received.");
+            return;
+        }
+
+        secureChannel.notifyOutOfBandAccepted();
+    }
+
+    @VisibleForTesting
+    @Nullable
+    SecureBleChannel getConnectedDeviceChannel() {
+        BleDevice connectedDevice = getConnectedDevice();
+        if (connectedDevice == null) {
+            return null;
+        }
+
+        return connectedDevice.mSecureChannel;
+    }
+
+    private void setDeviceId(@NonNull String deviceId) {
+        logd(TAG, "Setting device id: " + deviceId);
+        BleDevice connectedDevice = getConnectedDevice();
+        if (connectedDevice == null) {
+            disconnectWithError("Null connected device found when device id received.");
+            return;
+        }
+
+        connectedDevice.mDeviceId = deviceId;
+        mCallbacks.invoke(callback -> callback.onDeviceConnected(deviceId));
+    }
+
+    private void disconnectWithError(@NonNull String errorMessage) {
+        loge(TAG, errorMessage);
+        reset();
+    }
+
+    private void resetBluetoothAdapterName() {
+        if (mOriginalBluetoothName == null) {
+            return;
+        }
+        logd(TAG, "Changing bluetooth adapter name back to " + mOriginalBluetoothName + ".");
+        BluetoothAdapter.getDefaultAdapter().setName(mOriginalBluetoothName);
+        mOriginalBluetoothName = null;
+    }
+
+    private void verifyBluetoothNameRestored(@NonNull String expectedName) {
+        String currentName = BluetoothAdapter.getDefaultAdapter().getName();
+        if (expectedName.equals(currentName)) {
+            logd(TAG, "Bluetooth adapter name restoration completed successfully. Removing stored "
+                    + "adapter name.");
+            mStorage.removeStoredBluetoothName();
+            return;
+        }
+        logd(TAG, "Bluetooth adapter name restoration has not taken affect yet. Checking again in "
+                + ASSOCIATE_ADVERTISING_DELAY_MS + " milliseconds.");
+        mScheduler.schedule(
+                () -> verifyBluetoothNameRestored(expectedName),
+                ASSOCIATE_ADVERTISING_DELAY_MS, TimeUnit.MILLISECONDS);
+    }
+
+    private void addConnectedDevice(BluetoothDevice device, boolean isReconnect) {
+        EventLog.onDeviceConnected();
+        mBlePeripheralManager.stopAdvertising(mAdvertiseCallback);
+        mTimeoutHandler.removeCallbacks(mTimeoutRunnable);
+        mClientDeviceAddress = device.getAddress();
+        mClientDeviceName = device.getName();
+        if (mClientDeviceName == null) {
+            logd(TAG, "Device connected, but name is null; issuing request to retrieve device "
+                    + "name.");
+            mBlePeripheralManager.retrieveDeviceName(device);
+        }
+
+        BleDeviceMessageStream secureStream = new BleDeviceMessageStream(mBlePeripheralManager,
+                device, mWriteCharacteristic, mReadCharacteristic);
+        secureStream.setMaxWriteSize(mWriteSize);
+        SecureBleChannel secureChannel = new SecureBleChannel(secureStream, mStorage, isReconnect,
+                EncryptionRunnerFactory.newRunner());
+        secureChannel.registerCallback(mSecureChannelCallback);
+        BleDevice bleDevice = new BleDevice(device, /* gatt = */ null);
+        bleDevice.mSecureChannel = secureChannel;
+        addConnectedDevice(bleDevice);
+    }
+
+    private void setMtuSize(int mtuSize) {
+        mWriteSize = mtuSize - ATT_PROTOCOL_BYTES;
+        BleDevice connectedDevice = getConnectedDevice();
+        if (connectedDevice != null
+                && connectedDevice.mSecureChannel != null
+                && connectedDevice.mSecureChannel.getStream() != null) {
+            connectedDevice.mSecureChannel.getStream().setMaxWriteSize(mWriteSize);
+        }
+    }
+
+    private boolean isAssociating() {
+        return mAssociationCallback != null;
+    }
+
+    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 = null;
+                    BleDevice 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) {
+                        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;
+                    }
+                    mClientDeviceName = deviceName;
+                    BleDevice 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) {
+                    resetBluetoothAdapterName();
+                    addConnectedDevice(device, /* isReconnect = */ false);
+                    BleDevice connectedDevice = getConnectedDevice();
+                    if (connectedDevice == null || connectedDevice.mSecureChannel == null) {
+                        return;
+                    }
+                    connectedDevice.mSecureChannel.setShowVerificationCodeListener(
+                            code -> {
+                                if (!isAssociating()) {
+                                    loge(TAG, "No valid callback for association.");
+                                    return;
+                                }
+                                mAssociationCallback.onVerificationCodeAvailable(code);
+                            });
+                }
+
+                @Override
+                public void onRemoteDeviceDisconnected(BluetoothDevice device) {
+                    BleDevice 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));
+                    }
+                }
+            };
+
+    private final SecureBleChannel.Callback mSecureChannelCallback =
+            new SecureBleChannel.Callback() {
+                @Override
+                public void onSecureChannelEstablished() {
+                    BleDevice 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));
+                        if (mAssociationCallback != null) {
+                            mAssociationCallback.onAssociationCompleted(deviceId);
+                            mAssociationCallback = null;
+                        }
+                    }
+                    mCallbacks.invoke(callback -> callback.onSecureChannelEstablished(deviceId));
+                }
+
+                @Override
+                public void onEstablishSecureChannelFailure(int error) {
+                    BleDevice 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()) {
+                        mAssociationCallback.onAssociationError(error);
+                        disconnectWithError("Error while establishing secure connection.");
+                    }
+                }
+
+                @Override
+                public void onMessageReceived(DeviceMessage deviceMessage) {
+                    BleDevice 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.
+                }
+
+                @Override
+                public void onDeviceIdReceived(String deviceId) {
+                    setDeviceId(deviceId);
+                }
+            };
+
+    private final Runnable mTimeoutRunnable = new Runnable() {
+        @Override
+        public void run() {
+            logd(TAG, "Timeout period expired without a connection. Stopping advertisement.");
+            mBlePeripheralManager.stopAdvertising(mAdvertiseCallback);
+        }
+    };
+}
diff --git a/connected-device-lib/src/com/android/car/connecteddevice/ble/DeviceMessage.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/DeviceMessage.java
new file mode 100644
index 0000000..9d3ac48
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/DeviceMessage.java
@@ -0,0 +1,87 @@
+/*
+ * 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.ble;
+
+import static com.android.car.connecteddevice.BleStreamProtos.BleDeviceMessageProto.BleDeviceMessage;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.UUID;
+
+/** Holds the needed data from a {@link BleDeviceMessage}. */
+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/ble/SecureBleChannel.java b/connected-device-lib/src/com/android/car/connecteddevice/ble/SecureBleChannel.java
new file mode 100644
index 0000000..a821186
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/ble/SecureBleChannel.java
@@ -0,0 +1,477 @@
+/*
+ * 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.ble;
+
+import static com.android.car.connecteddevice.util.SafeLog.logd;
+import static com.android.car.connecteddevice.util.SafeLog.loge;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+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 com.android.car.connecteddevice.BleStreamProtos.BleOperationProto.OperationType;
+import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
+import com.android.car.connecteddevice.util.ByteUtils;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.security.SignatureException;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * Establishes a secure channel with {@link EncryptionRunner} over {@link BleDeviceMessageStream} as
+ * server side, sends and receives messages securely after the secure channel has been established.
+ */
+class SecureBleChannel {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "CHANNEL_ERROR" },
+            value = {
+                    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 the encryption key*/
+    static final int CHANNEL_ERROR_STORAGE_ERROR = 6;
+
+    @VisibleForTesting
+    static final byte[] CONFIRMATION_SIGNAL = "True".getBytes();
+
+    private static final String TAG = "SecureBleChannel";
+
+    private final BleDeviceMessageStream mStream;
+
+    private final ConnectedDeviceStorage mStorage;
+
+    private final boolean mIsReconnect;
+
+    private final EncryptionRunner mEncryptionRunner;
+
+    private final AtomicReference<Key> mEncryptionKey = new AtomicReference<>();
+
+    private @HandshakeState int mState = HandshakeState.UNKNOWN;
+
+    private String mDeviceId;
+
+    private Callback mCallback;
+
+    private ShowVerificationCodeListener mShowVerificationCodeListener;
+
+    SecureBleChannel(@NonNull BleDeviceMessageStream stream,
+            @NonNull ConnectedDeviceStorage storage) {
+        this(stream, storage, /* isReconnect = */ true, EncryptionRunnerFactory.newRunner());
+    }
+
+    SecureBleChannel(@NonNull BleDeviceMessageStream stream,
+            @NonNull ConnectedDeviceStorage storage, boolean isReconnect,
+            @NonNull EncryptionRunner encryptionRunner) {
+        mStream = stream;
+        mStorage = storage;
+        mIsReconnect = isReconnect;
+        mEncryptionRunner = encryptionRunner;
+        mEncryptionRunner.setIsReconnect(isReconnect);
+        mStream.setMessageReceivedListener(mStreamListener);
+    }
+
+    private void processHandshake(@NonNull byte[] message) throws HandshakeException {
+        switch (mState) {
+            case HandshakeState.UNKNOWN:
+                processHandshakeUnknown(message);
+                break;
+            case HandshakeState.IN_PROGRESS:
+                processHandshakeInProgress(message);
+                break;
+            case HandshakeState.RESUMING_SESSION:
+                processHandshakeResumingSession(message);
+                break;
+            default:
+                loge(TAG, "Encountered unexpected handshake state: " + mState + ". Received "
+                        + "message: " + ByteUtils.byteArrayToHexString(message) + ".");
+                notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
+        }
+    }
+
+    private void processHandshakeUnknown(@NonNull byte[] message) throws HandshakeException {
+        if (mDeviceId != null) {
+            logd(TAG, "Responding to handshake init request.");
+            HandshakeMessage handshakeMessage = mEncryptionRunner.respondToInitRequest(message);
+            mState = handshakeMessage.getHandshakeState();
+            sendHandshakeMessage(handshakeMessage.getNextMessage());
+            return;
+        }
+        UUID deviceId = ByteUtils.bytesToUUID(message);
+        if (deviceId == null) {
+            loge(TAG, "Received invalid device id. Ignoring.");
+            return;
+        }
+        mDeviceId = deviceId.toString();
+        if (mIsReconnect && !hasEncryptionKey(mDeviceId)) {
+            loge(TAG, "Attempted to reconnect device but no key found. Aborting secure channel.");
+            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
+            return;
+        }
+        notifyCallback(callback -> callback.onDeviceIdReceived(mDeviceId));
+        sendUniqueIdToClient();
+    }
+
+    private void processHandshakeInProgress(@NonNull byte[] message) throws HandshakeException {
+        logd(TAG, "Continuing handshake.");
+        HandshakeMessage handshakeMessage = mEncryptionRunner.continueHandshake(message);
+        mState = handshakeMessage.getHandshakeState();
+
+        boolean isValidStateForAssociation = !mIsReconnect
+                && mState == HandshakeState.VERIFICATION_NEEDED;
+        boolean isValidStateForReconnect = mIsReconnect
+                && mState == HandshakeState.RESUMING_SESSION;
+        if (!isValidStateForAssociation && !isValidStateForReconnect) {
+            loge(TAG, "processHandshakeInProgress: Encountered unexpected handshake state: "
+                    + mState + ".");
+            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_STATE);
+            return;
+        }
+
+        if (!isValidStateForAssociation) {
+            return;
+        }
+
+        String code = handshakeMessage.getVerificationCode();
+        if (code == null) {
+            loge(TAG, "Unable to get verification code.");
+            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_VERIFICATION);
+            return;
+        }
+
+        if (mShowVerificationCodeListener != null) {
+            logd(TAG, "Showing pairing code: " + code);
+            mShowVerificationCodeListener.showVerificationCode(code);
+        }
+    }
+
+    private void processHandshakeResumingSession(@NonNull byte[] message)
+            throws HandshakeException {
+        logd(TAG, "Start reconnection authentication.");
+        if (mDeviceId == null) {
+            loge(TAG, "processHandshakeResumingSession: Unable to resume session, device id is "
+                    + "null.");
+            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
+            return;
+        }
+
+        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 = mEncryptionRunner.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());
+        mEncryptionKey.set(newKey);
+        sendServerAuthToClient(handshakeMessage.getNextMessage());
+        notifyCallback(callback -> callback.onSecureChannelEstablished());
+    }
+
+    private void sendUniqueIdToClient() {
+        UUID uniqueId = mStorage.getUniqueId();
+        DeviceMessage deviceMessage = new DeviceMessage(/* recipient = */ null,
+                /* isMessageEncrypted = */ false, ByteUtils.uuidToBytes(uniqueId));
+        logd(TAG, "Sending car's device id of " + uniqueId + " to device.");
+        mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
+    }
+
+    private boolean hasEncryptionKey(@NonNull String id) {
+        return mStorage.getEncryptionKey(id) != null;
+    }
+
+    private void sendHandshakeMessage(@Nullable byte[] message) {
+        if (message == null) {
+            loge(TAG, "Unable to send next handshake message, message is null.");
+            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_MSG);
+            return;
+        }
+
+        logd(TAG, "Send handshake message: " + ByteUtils.byteArrayToHexString(message) + ".");
+        DeviceMessage deviceMessage = new DeviceMessage(/* recipient = */ null,
+                /* isMessageEncrypted = */ false, message);
+        mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
+    }
+
+    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;
+        }
+        DeviceMessage deviceMessage = new DeviceMessage(/* recipient = */ null,
+                /* isMessageEncrypted = */ false, message);
+        mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
+    }
+
+    /**
+     * Send an encrypted message.
+     * <p>Note: This should be called only after the secure channel has been established.</p>
+     *
+     * @param deviceMessage The {@link DeviceMessage} to encrypt and send.
+     */
+    void sendEncryptedMessage(@NonNull DeviceMessage deviceMessage) throws IllegalStateException {
+        if (!deviceMessage.isMessageEncrypted()) {
+            loge(TAG, "Encryption not required for this message " + deviceMessage + ".");
+            return;
+        }
+        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);
+        mStream.writeMessage(deviceMessage, OperationType.CLIENT_MESSAGE);
+    }
+
+    /**
+     * 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.
+     */
+    void notifyOutOfBandAccepted() {
+        HandshakeMessage message;
+        try {
+            message = mEncryptionRunner.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();
+        mStorage.saveEncryptionKey(mDeviceId, localKey.asBytes());
+        mEncryptionKey.set(localKey);
+        if (mDeviceId == null) {
+            loge(TAG, "Unable to finish association, device id is null.");
+            notifySecureChannelFailure(CHANNEL_ERROR_INVALID_DEVICE_ID);
+            return;
+        }
+        logd(TAG, "Pairing code successfully verified and encryption key saved. Sending "
+                + "confirmation to device.");
+        notifyCallback(Callback::onSecureChannelEstablished);
+        DeviceMessage deviceMessage = new DeviceMessage(/* recipient = */ null,
+                /* isMessageEncrypted = */ false, CONFIRMATION_SIGNAL);
+        mStream.writeMessage(deviceMessage, OperationType.ENCRYPTION_HANDSHAKE);
+    }
+
+    /** Get the BLE stream backing this channel. */
+    @NonNull
+    BleDeviceMessageStream getStream() {
+        return mStream;
+    }
+
+    /**Set the listener that notifies to show verification code. {@code null} to clear.*/
+    void setShowVerificationCodeListener(@Nullable ShowVerificationCodeListener listener) {
+        mShowVerificationCodeListener = listener;
+    }
+
+    @VisibleForTesting
+    @Nullable
+    ShowVerificationCodeListener getShowVerificationCodeListener() {
+        return mShowVerificationCodeListener;
+    }
+
+    /** Register a callback that notifies secure channel events. */
+    void registerCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    /** Unregister a callback. */
+    void unregisterCallback(Callback callback) {
+        if (callback == mCallback) {
+            mCallback = null;
+        }
+    }
+
+    @VisibleForTesting
+    @Nullable
+    Callback getCallback() {
+        return mCallback;
+    }
+
+    private void notifyCallback(Consumer<Callback> notification) {
+        if (mCallback != null) {
+            notification.accept(mCallback);
+        }
+    }
+
+    private void notifySecureChannelFailure(@ChannelError int error) {
+        loge(TAG, "Secure channel error: " + error);
+        notifyCallback(callback -> callback.onEstablishSecureChannelFailure(error));
+    }
+
+    private final BleDeviceMessageStream.MessageReceivedListener mStreamListener =
+            new BleDeviceMessageStream.MessageReceivedListener() {
+                @Override
+                public void onMessageReceived(DeviceMessage deviceMessage,
+                        OperationType operationType) {
+                    byte[] message = deviceMessage.getMessage();
+                    switch(operationType) {
+                        case ENCRYPTION_HANDSHAKE:
+                            logd(TAG, "Message received and handed off to handshake.");
+                            try {
+                                processHandshake(message);
+                            } catch (HandshakeException e) {
+                                loge(TAG, "Handshake failed.", e);
+                                notifyCallback(callback -> callback.onEstablishSecureChannelFailure(
+                                        CHANNEL_ERROR_INVALID_HANDSHAKE));
+                            }
+                            break;
+                        case CLIENT_MESSAGE:
+                            logd(TAG, "Received client message.");
+                            if (!deviceMessage.isMessageEncrypted()) {
+                                notifyCallback(callback -> callback.onMessageReceived(
+                                        deviceMessage));
+                                return;
+                            }
+                            Key key = mEncryptionKey.get();
+                            if (key == null) {
+                                loge(TAG, "Received encrypted message before secure channel has "
+                                        + "been established.");
+                                notifyCallback(callback -> callback.onMessageReceivedError(null));
+                                return;
+                            }
+                            try {
+                                byte[] decryptedPayload =
+                                        key.decryptData(deviceMessage.getMessage());
+                                deviceMessage.setMessage(decryptedPayload);
+                                notifyCallback(
+                                        callback -> callback.onMessageReceived(deviceMessage));
+                            } catch (SignatureException e) {
+                                loge(TAG, "Could not decrypt client credentials.", e);
+                                notifyCallback(callback -> callback.onMessageReceivedError(e));
+                            }
+                            break;
+                        default:
+                            loge(TAG, "Received unexpected operation type: " + operationType + ".");
+                    }
+                }
+            };
+
+    /**
+     * Callbacks that will be invoked during establishing secure channel, sending and receiving
+     * messages securely.
+     */
+    interface Callback {
+        /**
+         * Invoked when secure channel has been established successfully.
+         */
+        void onSecureChannelEstablished();
+
+        /**
+         * Invoked when a {@link ChannelError} has been encountered in attempting to establish
+         * a secure channel.
+         *
+         * @param error The failure indication.
+         */
+        void onEstablishSecureChannelFailure(@SecureBleChannel.ChannelError int error);
+
+        /**
+         * Invoked when a complete message is received securely from the client and decrypted.
+         *
+         * @param deviceMessage The {@link DeviceMessage} with decrypted message.
+         */
+        void onMessageReceived(@NonNull DeviceMessage deviceMessage);
+
+        /**
+         * Invoked when there was an error during a processing or decrypting of a client message.
+         *
+         * @param exception The error.
+         */
+        void onMessageReceivedError(@Nullable Exception exception);
+
+        /**
+         * Invoked when the device id was received from the client.
+         *
+         * @param deviceId The unique device id of client.
+         */
+        void onDeviceIdReceived(@NonNull String deviceId);
+    }
+
+    /**
+     * Listener that will be invoked to display verification code.
+     */
+    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/model/AssociatedDevice.java b/connected-device-lib/src/com/android/car/connecteddevice/model/AssociatedDevice.java
new file mode 100644
index 0000000..88fce6c
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/model/AssociatedDevice.java
@@ -0,0 +1,96 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.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
new file mode 100644
index 0000000..d65f97d
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/model/ConnectedDevice.java
@@ -0,0 +1,96 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.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/storage/AssociatedDeviceDao.java b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceDao.java
new file mode 100644
index 0000000..c041d58
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceDao.java
@@ -0,0 +1,66 @@
+/*
+ * 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);
+}
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
new file mode 100644
index 0000000..1c5182c
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceEntity.java
@@ -0,0 +1,64 @@
+/*
+ * 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
new file mode 100644
index 0000000..6cd791f
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/storage/AssociatedDeviceKeyEntity.java
@@ -0,0 +1,41 @@
+/*
+ * 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
new file mode 100644
index 0000000..3671440
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceDatabase.java
@@ -0,0 +1,28 @@
+/*
+ * 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 }, version = 1,
+        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
new file mode 100644
index 0000000..433ee1d
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorage.java
@@ -0,0 +1,483 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.Nullable;
+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.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.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.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
+
+/** 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 = ";";
+    // 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;
+
+    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      did of trusted 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.");
+    }
+
+    /**
+     * 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
new file mode 100644
index 0000000..3d07227
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/util/ByteUtils.java
@@ -0,0 +1,159 @@
+/*
+ * 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.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+
+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
new file mode 100644
index 0000000..5dcd829
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/util/EventLog.java
@@ -0,0 +1,65 @@
+/*
+ * 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
new file mode 100644
index 0000000..e18366b
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/util/RemoteCallbackBinder.java
@@ -0,0 +1,74 @@
+/*
+ * 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) {
+        return mCallbackBinder.equals(obj);
+    }
+
+    @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
new file mode 100644
index 0000000..6ab18ce
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/util/SafeLog.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.connecteddevice.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+/**
+ * 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
new file mode 100644
index 0000000..6748bba
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/util/ScanDataAnalyzer.java
@@ -0,0 +1,124 @@
+/*
+ * 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.annotation.NonNull;
+import android.bluetooth.le.ScanResult;
+
+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
new file mode 100644
index 0000000..b3d3ef1
--- /dev/null
+++ b/connected-device-lib/src/com/android/car/connecteddevice/util/ThreadSafeCallbacks.java
@@ -0,0 +1,60 @@
+/*
+ * 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.CallbackExecutor;
+import android.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 @CallbackExecutor 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
new file mode 100644
index 0000000..9cf29ba
--- /dev/null
+++ b/connected-device-lib/tests/unit/Android.bp
@@ -0,0 +1,50 @@
+//
+// 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",
+        "truth-prebuilt",
+    ],
+
+    jni_libs: [
+        // For mockito extended
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+
+    platform_apis: true,
+
+    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
new file mode 100644
index 0000000..9863ccf
--- /dev/null
+++ b/connected-device-lib/tests/unit/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<!--
+  ~ 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
new file mode 100644
index 0000000..4543058
--- /dev/null
+++ b/connected-device-lib/tests/unit/README.md
@@ -0,0 +1,24 @@
+# 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
new file mode 100644
index 0000000..579fe7d
--- /dev/null
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ConnectedDeviceManagerTest.java
@@ -0,0 +1,746 @@
+/*
+ * 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.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+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 android.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.ble.CarBleCentralManager;
+import com.android.car.connecteddevice.ble.CarBleManager;
+import com.android.car.connecteddevice.ble.CarBlePeripheralManager;
+import com.android.car.connecteddevice.ble.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.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 static final int DEFAULT_RECONNECT_TIMEOUT = 5;
+
+    private final Executor mCallbackExecutor = Executors.newSingleThreadExecutor();
+
+    private final UUID mRecipientId = UUID.randomUUID();
+
+    @Mock
+    private ConnectedDeviceStorage mMockStorage;
+
+    @Mock
+    private CarBlePeripheralManager mMockPeripheralManager;
+
+    @Mock
+    private CarBleCentralManager mMockCentralManager;
+
+    private ConnectedDeviceManager mConnectedDeviceManager;
+
+    private MockitoSession mMockingSession;
+
+    private AssociatedDeviceCallback mAssociatedDeviceCallback;
+
+    @Before
+    public void setUp() {
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+        ArgumentCaptor<AssociatedDeviceCallback> callbackCaptor = ArgumentCaptor
+                .forClass(AssociatedDeviceCallback.class);
+        mConnectedDeviceManager = new ConnectedDeviceManager(mMockStorage, mMockCentralManager,
+            mMockPeripheralManager, DEFAULT_RECONNECT_TIMEOUT);
+        verify(mMockStorage).setAssociatedDeviceCallback(callbackCaptor.capture());
+        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(mMockCentralManager);
+        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, mMockCentralManager);
+        assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
+    }
+
+    @Test
+    public void getActiveUserConnectedDevices_reflectsSecureChannelEstablished() {
+        String deviceId = connectNewDevice(mMockCentralManager);
+        mConnectedDeviceManager.onSecureChannelEstablished(deviceId, mMockCentralManager);
+        ConnectedDevice connectedDevice =
+                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
+        assertThat(connectedDevice.hasSecureChannel()).isTrue();
+    }
+
+    @Test
+    public void getActiveUserConnectedDevices_excludesDisconnectedDevice() {
+        String deviceId = connectNewDevice(mMockCentralManager);
+        mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockCentralManager);
+        assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).isEmpty();
+    }
+
+    @Test
+    public void getActiveUserConnectedDevices_unaffectedByOtherManagerDisconnect() {
+        String deviceId = connectNewDevice(mMockCentralManager);
+        mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
+        assertThat(mConnectedDeviceManager.getActiveUserConnectedDevices()).hasSize(1);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void sendMessageSecurely_throwsIllegalStateExceptionIfNoSecureChannel() {
+        connectNewDevice(mMockCentralManager);
+        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(mMockCentralManager);
+        mConnectedDeviceManager.onSecureChannelEstablished(deviceId, mMockCentralManager);
+        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(mMockCentralManager).sendMessage(eq(deviceId), messageCaptor.capture());
+        assertThat(messageCaptor.getValue().isMessageEncrypted()).isTrue();
+    }
+
+    @Test
+    public void sendMessageSecurely_doesNotSendIfDeviceDisconnected() {
+        String deviceId = connectNewDevice(mMockCentralManager);
+        ConnectedDevice device = mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
+        mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockCentralManager);
+        UUID recipientId = UUID.randomUUID();
+        byte[] message = ByteUtils.randomBytes(10);
+        mConnectedDeviceManager.sendMessageSecurely(device, recipientId, message);
+        verify(mMockCentralManager, times(0)).sendMessage(eq(deviceId), any(DeviceMessage.class));
+    }
+
+    @Test
+    public void sendMessageUnsecurely_sendsMessageWithoutEncryption() {
+        String deviceId = connectNewDevice(mMockCentralManager);
+        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(mMockCentralManager).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(mMockCentralManager);
+        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, mMockCentralManager);
+        assertThat(tryAcquire(semaphore)).isFalse();
+    }
+
+    @Test
+    public void connectionCallback_onDeviceConnectedNotInvokedForDifferentBleManager()
+            throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        String deviceId = connectNewDevice(mMockPeripheralManager);
+        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
+        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
+                mCallbackExecutor);
+        mConnectedDeviceManager.addConnectedDevice(deviceId, mMockCentralManager);
+        assertThat(tryAcquire(semaphore)).isFalse();
+    }
+
+    @Test
+    public void connectionCallback_onDeviceDisconnectedInvokedForActiveUserDevice()
+            throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        String deviceId = connectNewDevice(mMockCentralManager);
+        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
+        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
+                mCallbackExecutor);
+        mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockCentralManager);
+        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, mMockCentralManager);
+        ConnectionCallback connectionCallback = createConnectionCallback(semaphore);
+        mConnectedDeviceManager.registerActiveUserConnectionCallback(connectionCallback,
+                mCallbackExecutor);
+        mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockCentralManager);
+        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(mMockCentralManager);
+        assertThat(tryAcquire(semaphore)).isFalse();
+    }
+
+    @Test
+    public void registerDeviceCallback_blacklistsDuplicateRecipientId()
+            throws InterruptedException {
+        connectNewDevice(mMockCentralManager);
+        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(mMockCentralManager);
+        ConnectedDevice connectedDevice =
+                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
+        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
+        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
+                deviceCallback, mCallbackExecutor);
+        mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId(),
+                mMockCentralManager);
+        connectedDevice =
+                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
+        assertThat(tryAcquire(semaphore)).isTrue();
+        verify(deviceCallback).onSecureChannelEstablished(connectedDevice);
+    }
+
+    @Test
+    public void deviceCallback_onSecureChannelEstablishedNotInvokedWithSecondBleManager()
+            throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        connectNewDevice(mMockCentralManager);
+        ConnectedDevice connectedDevice =
+                mConnectedDeviceManager.getActiveUserConnectedDevices().get(0);
+        mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId(),
+                mMockCentralManager);
+        DeviceCallback deviceCallback = createDeviceCallback(semaphore);
+        mConnectedDeviceManager.registerDeviceCallback(connectedDevice, mRecipientId,
+                deviceCallback, mCallbackExecutor);
+        mConnectedDeviceManager.onSecureChannelEstablished(connectedDevice.getDeviceId(),
+                mMockPeripheralManager);
+        assertThat(tryAcquire(semaphore)).isFalse();
+    }
+
+    @Test
+    public void deviceCallback_onMessageReceivedInvokedForSameRecipientId()
+            throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        connectNewDevice(mMockCentralManager);
+        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(mMockCentralManager);
+        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(mMockCentralManager);
+        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(mMockCentralManager);
+        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(),
+                mMockPeripheralManager);
+        assertThat(tryAcquire(semaphore)).isFalse();
+    }
+
+    @Test
+    public void registerDeviceCallback_sendsMissedMessageAfterRegistration()
+            throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        connectNewDevice(mMockCentralManager);
+        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_doesNotSendMissedMessageForDifferentRecipient()
+            throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        connectNewDevice(mMockCentralManager);
+        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(mMockCentralManager);
+        connectNewDevice(mMockCentralManager);
+        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, mMockPeripheralManager);
+        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, mMockPeripheralManager);
+        mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
+        verify(mMockPeripheralManager, timeout(1000))
+                .connectToDevice(eq(UUID.fromString(deviceId)), anyInt());
+    }
+
+    @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, mMockPeripheralManager);
+        mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
+        verify(mMockPeripheralManager, timeout(1000))
+                .connectToDevice(eq(UUID.fromString(userDeviceId)), anyInt());
+    }
+
+    @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, mMockPeripheralManager);
+        mConnectedDeviceManager.addConnectedDevice(userDeviceId, mMockCentralManager);
+        mConnectedDeviceManager.removeConnectedDevice(deviceId, mMockPeripheralManager);
+        verify(mMockPeripheralManager, timeout(1000).times(0))
+                .connectToDevice(eq(UUID.fromString(userDeviceId)), anyInt());
+    }
+
+    @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(mMockPeripheralManager);
+        mConnectedDeviceManager.removeActiveUserAssociatedDevice(deviceId);
+        verify(mMockPeripheralManager).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(mMockPeripheralManager);
+        mConnectedDeviceManager.disableAssociatedDeviceConnection(deviceId);
+        verify(mMockPeripheralManager).disconnectDevice(deviceId);
+    }
+
+    @Test
+    public void onMessageReceived_deliversMessageIfDelegateIsNull() throws InterruptedException {
+        connectNewDevice(mMockCentralManager);
+        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(mMockCentralManager);
+        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(mMockCentralManager);
+        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(@NonNull CarBleManager carBleManager) {
+        String deviceId = UUID.randomUUID().toString();
+        AssociatedDevice device = new AssociatedDevice(deviceId, TEST_DEVICE_ADDRESS,
+                TEST_DEVICE_NAME, /* isConnectionEnabled = */ true);
+        when(mMockStorage.getActiveUserAssociatedDevices()).thenReturn(
+                Collections.singletonList(device));
+        when(mMockStorage.getActiveUserAssociatedDeviceIds()).thenReturn(
+                Collections.singletonList(deviceId));
+        mConnectedDeviceManager.addConnectedDevice(deviceId, carBleManager);
+        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/ble/BleDeviceMessageStreamTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/BleDeviceMessageStreamTest.java
new file mode 100644
index 0000000..b45b6f2
--- /dev/null
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/BleDeviceMessageStreamTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.ble;
+
+import static com.android.car.connecteddevice.BleStreamProtos.BleDeviceMessageProto.BleDeviceMessage;
+import static com.android.car.connecteddevice.BleStreamProtos.BleOperationProto.OperationType;
+import static com.android.car.connecteddevice.BleStreamProtos.BlePacketProto.BlePacket;
+import static com.android.car.connecteddevice.ble.BleDeviceMessageStream.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.mockitoSession;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.NonNull;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.connecteddevice.util.ByteUtils;
+import com.android.car.protobuf.ByteString;
+
+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.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 BleDeviceMessageStreamTest {
+
+    private static final String TAG = "BleDeviceMessageStreamTest";
+
+    private BleDeviceMessageStream mStream;
+
+    @Mock
+    private BlePeripheralManager mMockBlePeripheralManager;
+
+    @Mock
+    private BluetoothDevice mMockBluetoothDevice;
+
+    @Mock
+    private BluetoothGattCharacteristic mMockWriteCharacteristic;
+
+    @Mock
+    private BluetoothGattCharacteristic mMockReadCharacteristic;
+
+    private MockitoSession mMockingSession;
+
+    @Before
+    public void setup() {
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+
+        mStream = new BleDeviceMessageStream(mMockBlePeripheralManager, mMockBluetoothDevice,
+                mMockWriteCharacteristic, mMockReadCharacteristic);
+    }
+
+    @After
+    public void cleanup() {
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+    }
+
+    @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(750);
+        List<BlePacket> packets1 = createPackets(data);
+        List<BlePacket> 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);
+        MessageReceivedListener listener = createMessageReceivedListener(semaphore);
+        mStream.setMessageReceivedListener(listener);
+        byte[] data = ByteUtils.randomBytes(750);
+        List<BlePacket> packets = createPackets(data);
+        for (int i = 0; i < packets.size() - 1; i++) {
+            mStream.processPacket(packets.get(i));
+        }
+        assertThat(tryAcquire(semaphore)).isFalse();
+    }
+
+    @NonNull
+    private List<BlePacket> createPackets(byte[] data) {
+        try {
+            BleDeviceMessage message = BleDeviceMessage.newBuilder()
+                    .setPayload(ByteString.copyFrom(data))
+                    .setOperation(OperationType.CLIENT_MESSAGE)
+                    .build();
+            return BlePacketFactory.makeBlePackets(message.toByteArray(),
+                    ThreadLocalRandom.current().nextInt(), 500);
+        } catch (Exception e) {
+            assertWithMessage("Uncaught exception while making packets.").fail();
+            return new ArrayList<>();
+        }
+    }
+
+    private void processMessage(byte[] data) {
+        List<BlePacket> packets = createPackets(data);
+        for (BlePacket packet : packets) {
+            mStream.processPacket(packet);
+        }
+    }
+
+    private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
+        return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
+    }
+
+    @NonNull
+    private MessageReceivedListener createMessageReceivedListener(
+            Semaphore semaphore) {
+        return spy((deviceMessage, operationType) -> semaphore.release());
+    }
+
+}
diff --git a/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/BlePacketFactoryTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/BlePacketFactoryTest.java
new file mode 100644
index 0000000..8e8682f
--- /dev/null
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/BlePacketFactoryTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.ble;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.connecteddevice.BleStreamProtos.BlePacketProto.BlePacket;
+
+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 BlePacketFactoryTest {
+    @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(BlePacketFactory.getPacketHeaderSize(totalPackets, messageId, payloadSize))
+                .isEqualTo(expectedHeaderSize);
+    }
+
+    @Test
+    public void testGetTotalPackets_withVarintSize1_returnsCorrectPackets()
+            throws BlePacketFactoryException {
+        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(BlePacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
+                .isEqualTo(expectedTotalPackets);
+    }
+
+    @Test
+    public void testGetTotalPackets_withVarintSize2_returnsCorrectPackets()
+            throws BlePacketFactoryException {
+        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(BlePacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
+                .isEqualTo(expectedTotalPackets);
+    }
+
+    @Test
+    public void testGetTotalPackets_withVarintSize3_returnsCorrectPackets()
+            throws BlePacketFactoryException {
+        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(BlePacketFactory.getTotalPacketNumber(messageId, payloadSize, maxSize))
+                .isEqualTo(expectedTotalPackets);
+    }
+
+    @Test
+    public void testGetTotalPackets_withVarintSize4_returnsCorrectPackets()
+            throws BlePacketFactoryException {
+        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(BlePacketFactory.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<BlePacket> packets =
+                BlePacketFactory.makeBlePackets(payload, /* mesageId= */ 1, maxSize);
+
+        assertThat(packets).hasSize(1);
+
+        ByteArrayOutputStream reconstructedPayload = new ByteArrayOutputStream();
+
+        // Combine together all the payloads within the BlePackets.
+        for (BlePacket 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<BlePacket> packets =
+                BlePacketFactory.makeBlePackets(payload, /* mesageId= */ 1, maxSize);
+
+        assertThat(packets.size()).isGreaterThan(1);
+
+        ByteArrayOutputStream reconstructedPayload = new ByteArrayOutputStream();
+
+        // Combine together all the payloads within the BlePackets.
+        for (BlePacket 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/ble/CarBlePeripheralManagerTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java
new file mode 100644
index 0000000..986f11e
--- /dev/null
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/CarBlePeripheralManagerTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.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.spy;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.annotation.NonNull;
+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.car.encryptionrunner.EncryptionRunnerFactory;
+import android.car.encryptionrunner.Key;
+import android.os.ParcelUuid;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.connecteddevice.AssociationCallback;
+import com.android.car.connecteddevice.model.AssociatedDevice;
+import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
+import com.android.car.connecteddevice.util.ByteUtils;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+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 class CarBlePeripheralManagerTest {
+    private static final UUID ASSOCIATION_SERVICE_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 byte[] TEST_KEY = "Key".getBytes();
+    private static String sAdapterName;
+
+    @Mock private BlePeripheralManager mMockPeripheralManager;
+    @Mock private ConnectedDeviceStorage mMockStorage;
+
+    private CarBlePeripheralManager mCarBlePeripheralManager;
+
+    private MockitoSession mMockitoSession;
+
+    @BeforeClass
+    public static void beforeSetUp() {
+        sAdapterName = BluetoothAdapter.getDefaultAdapter().getName();
+    }
+    @Before
+    public void setUp() {
+        mMockitoSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+        mCarBlePeripheralManager = new CarBlePeripheralManager(mMockPeripheralManager, mMockStorage,
+                ASSOCIATION_SERVICE_UUID, WRITE_UUID, READ_UUID);
+    }
+
+    @After
+    public void tearDown() {
+        if (mCarBlePeripheralManager != null) {
+            mCarBlePeripheralManager.stop();
+        }
+        if (mMockitoSession != null) {
+            mMockitoSession.finishMocking();
+        }
+    }
+
+    @AfterClass
+    public static void afterTearDown() {
+        BluetoothAdapter.getDefaultAdapter().setName(sAdapterName);
+    }
+
+    @Test
+    public void testStartAssociationAdvertisingSuccess() {
+        Semaphore semaphore = new Semaphore(0);
+        AssociationCallback callback = createAssociationCallback(semaphore);
+        String testDeviceName = getNameForAssociation();
+        startAssociation(callback, testDeviceName);
+        ArgumentCaptor<AdvertiseData> dataCaptor = ArgumentCaptor.forClass(AdvertiseData.class);
+        verify(mMockPeripheralManager, timeout(3000)).startAdvertising(any(),
+                dataCaptor.capture(), any());
+        AdvertiseData data = dataCaptor.getValue();
+        assertThat(data.getIncludeDeviceName()).isTrue();
+        ParcelUuid expected = new ParcelUuid(ASSOCIATION_SERVICE_UUID);
+        assertThat(data.getServiceUuids().get(0)).isEqualTo(expected);
+        assertThat(BluetoothAdapter.getDefaultAdapter().getName()).isEqualTo(testDeviceName);
+    }
+
+    @Test
+    public void testStartAssociationAdvertisingFailure() throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        AssociationCallback callback = createAssociationCallback(semaphore);
+        startAssociation(callback, getNameForAssociation());
+        ArgumentCaptor<AdvertiseCallback> callbackCaptor =
+                ArgumentCaptor.forClass(AdvertiseCallback.class);
+        verify(mMockPeripheralManager, timeout(3000))
+                .startAdvertising(any(), any(), callbackCaptor.capture());
+        AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
+        int testErrorCode = 2;
+        advertiseCallback.onStartFailure(testErrorCode);
+        assertThat(tryAcquire(semaphore)).isTrue();
+        verify(callback).onAssociationStartFailure();
+    }
+
+    @Test
+    public void testNotifyAssociationSuccess() throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        AssociationCallback callback = createAssociationCallback(semaphore);
+        String testDeviceName = getNameForAssociation();
+        startAssociation(callback, testDeviceName);
+        ArgumentCaptor<AdvertiseCallback> callbackCaptor =
+                ArgumentCaptor.forClass(AdvertiseCallback.class);
+        verify(mMockPeripheralManager, timeout(3000))
+                .startAdvertising(any(), any(), callbackCaptor.capture());
+        AdvertiseCallback advertiseCallback = callbackCaptor.getValue();
+        AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
+        advertiseCallback.onStartSuccess(settings);
+        assertThat(tryAcquire(semaphore)).isTrue();
+        verify(callback).onAssociationStartSuccess(eq(testDeviceName));
+    }
+
+    @Test
+    public void testShowVerificationCode() throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        AssociationCallback callback = createAssociationCallback(semaphore);
+        SecureBleChannel 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);
+        SecureBleChannel channel = getChannelForAssociation(callback);
+        SecureBleChannel.Callback channelCallback = channel.getCallback();
+        assertThat(channelCallback).isNotNull();
+        channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
+        Key key = EncryptionRunnerFactory.newDummyRunner().keyOf(TEST_KEY);
+        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()));
+    }
+
+    @Test
+    public void testAssociationFailure_channelError() throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        AssociationCallback callback = createAssociationCallback(semaphore);
+        SecureBleChannel channel = getChannelForAssociation(callback);
+        SecureBleChannel.Callback channelCallback = channel.getCallback();
+        int testErrorCode = 1;
+        assertThat(channelCallback).isNotNull();
+        channelCallback.onDeviceIdReceived(TEST_REMOTE_DEVICE_ID.toString());
+        channelCallback.onEstablishSecureChannelFailure(testErrorCode);
+        assertThat(tryAcquire(semaphore)).isTrue();
+        verify(callback).onAssociationError(eq(testErrorCode));
+    }
+
+    @Test
+    public void connectToDevice_stopsAdvertisingAfterTimeout() {
+        int timeoutSeconds = 2;
+        mCarBlePeripheralManager.connectToDevice(UUID.randomUUID(), timeoutSeconds);
+        ArgumentCaptor<AdvertiseCallback> callbackCaptor =
+                ArgumentCaptor.forClass(AdvertiseCallback.class);
+        verify(mMockPeripheralManager).startAdvertising(any(), any(), callbackCaptor.capture());
+        callbackCaptor.getValue().onStartSuccess(null);
+        verify(mMockPeripheralManager, timeout(TimeUnit.SECONDS.toMillis(timeoutSeconds + 1)))
+                .stopAdvertising(any(AdvertiseCallback.class));
+    }
+
+    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 SecureBleChannel getChannelForAssociation(AssociationCallback callback) {
+        BlePeripheralManager.Callback bleManagerCallback = startAssociation(callback,
+                getNameForAssociation());
+        BluetoothDevice bluetoothDevice = BluetoothAdapter.getDefaultAdapter()
+                .getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS);
+        bleManagerCallback.onRemoteDeviceConnected(bluetoothDevice);
+        return mCarBlePeripheralManager.getConnectedDeviceChannel();
+    }
+
+    private boolean tryAcquire(Semaphore semaphore) throws InterruptedException {
+        return semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
+    }
+
+    private String getNameForAssociation() {
+        return ByteUtils.generateRandomNumberString(DEVICE_NAME_LENGTH_LIMIT);
+
+    }
+
+    @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/ble/SecureBleChannelTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/SecureBleChannelTest.java
new file mode 100644
index 0000000..2960e49
--- /dev/null
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/ble/SecureBleChannelTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.ble;
+
+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.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.car.encryptionrunner.DummyEncryptionRunner;
+import android.car.encryptionrunner.EncryptionRunnerFactory;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.car.connecteddevice.BleStreamProtos.BleOperationProto.OperationType;
+import com.android.car.connecteddevice.ble.BleDeviceMessageStream.MessageReceivedListener;
+import com.android.car.connecteddevice.storage.ConnectedDeviceStorage;
+import com.android.car.connecteddevice.util.ByteUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.UUID;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public final class SecureBleChannelTest {
+    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 SecureBleChannel mChannel;
+    private MessageReceivedListener mMessageReceivedListener;
+
+    @Mock private BleDeviceMessageStream mStreamMock;
+    @Mock private ConnectedDeviceStorage mStorageMock;
+    @Mock private SecureBleChannel.ShowVerificationCodeListener mShowVerificationCodeListenerMock;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mStorageMock.getUniqueId()).thenReturn(SERVER_DEVICE_ID);
+    }
+
+    @Test
+    public void testEncryptionHandshake_Association() throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        ChannelCallback callbackSpy = spy(new ChannelCallback(semaphore));
+        setUpSecureBleChannel_Association(callbackSpy);
+        ArgumentCaptor<String> deviceIdCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<DeviceMessage> messageCaptor =
+                ArgumentCaptor.forClass(DeviceMessage.class);
+
+        sendDeviceId();
+        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
+        verify(callbackSpy).onDeviceIdReceived(deviceIdCaptor.capture());
+        verify(mStreamMock).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());
+
+        initHandshakeMessage();
+        verify(mStreamMock, times(2)).writeMessage(messageCaptor.capture(), any());
+        byte[] response = messageCaptor.getValue().getMessage();
+        assertThat(response).isEqualTo(DummyEncryptionRunner.INIT_RESPONSE.getBytes());
+
+        respondToContinueMessage();
+        verify(mShowVerificationCodeListenerMock).showVerificationCode(anyString());
+
+        mChannel.notifyOutOfBandAccepted();
+        verify(mStreamMock, times(3)).writeMessage(messageCaptor.capture(), any());
+        byte[] confirmMessage = messageCaptor.getValue().getMessage();
+        assertThat(confirmMessage).isEqualTo(SecureBleChannel.CONFIRMATION_SIGNAL);
+        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));
+        setUpSecureBleChannel_Association(callbackSpy);
+
+        sendDeviceId();
+        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
+
+        // Wrong init handshake message
+        respondToContinueMessage();
+        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
+        verify(callbackSpy).onEstablishSecureChannelFailure(
+                eq(SecureBleChannel.CHANNEL_ERROR_INVALID_HANDSHAKE)
+        );
+    }
+
+    @Test
+    public void testEncryptionHandshake_Association_wrongRespondToContinueMessage()
+            throws InterruptedException {
+        Semaphore semaphore = new Semaphore(0);
+        ChannelCallback callbackSpy = spy(new ChannelCallback(semaphore));
+        setUpSecureBleChannel_Association(callbackSpy);
+
+        sendDeviceId();
+        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
+
+        initHandshakeMessage();
+
+        // Wrong respond to continue message
+        initHandshakeMessage();
+        assertThat(semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)).isTrue();
+        verify(callbackSpy).onEstablishSecureChannelFailure(
+                eq(SecureBleChannel.CHANNEL_ERROR_INVALID_HANDSHAKE)
+        );
+    }
+
+    private void setUpSecureBleChannel_Association(ChannelCallback callback) {
+        mChannel = new SecureBleChannel(
+                mStreamMock,
+                mStorageMock,
+                /* isReconnect = */ false,
+                EncryptionRunnerFactory.newDummyRunner()
+        );
+        mChannel.registerCallback(callback);
+        mChannel.setShowVerificationCodeListener(mShowVerificationCodeListenerMock);
+        ArgumentCaptor<MessageReceivedListener> listenerCaptor =
+                ArgumentCaptor.forClass(MessageReceivedListener.class);
+        verify(mStreamMock).setMessageReceivedListener(listenerCaptor.capture());
+        mMessageReceivedListener = listenerCaptor.getValue();
+    }
+
+    private void sendDeviceId() {
+        DeviceMessage message = new DeviceMessage(
+                /* recipient = */ null,
+                /* isMessageEncrypted = */ false,
+                ByteUtils.uuidToBytes(CLIENT_DEVICE_ID)
+        );
+        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
+    }
+
+    private void initHandshakeMessage() {
+        DeviceMessage message = new DeviceMessage(
+                /* recipient = */ null,
+                /* isMessageEncrypted = */ false,
+                DummyEncryptionRunner.INIT.getBytes()
+        );
+        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
+    }
+
+    private void respondToContinueMessage() {
+        DeviceMessage message = new DeviceMessage(
+                /* recipient = */ null,
+                /* isMessageEncrypted = */ false,
+                DummyEncryptionRunner.CLIENT_RESPONSE.getBytes()
+        );
+        mMessageReceivedListener.onMessageReceived(message, OperationType.ENCRYPTION_HANDSHAKE);
+    }
+
+    /**
+     * Add the thread control logic into {@link SecureBleChannel.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.
+     */
+    class ChannelCallback implements SecureBleChannel.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/storage/ConnectedDeviceStorageTest.java b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorageTest.java
new file mode 100644
index 0000000..9547bfb
--- /dev/null
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/storage/ConnectedDeviceStorageTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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 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.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();
+    }
+
+    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
new file mode 100644
index 0000000..92e8d34
--- /dev/null
+++ b/connected-device-lib/tests/unit/src/com/android/car/connecteddevice/util/ScanDataAnalyzerTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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();
+    }
+}