Merge tag 'android-security-10.0.0_r53' into int/10/fp2

Android security 10.0.0 release 53

* tag 'android-security-10.0.0_r53':

Change-Id: Id765038c1737a12ddd57236458e54145c1b06cd8
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/OWNERS b/OWNERS
index 610f429..c1e7ec6 100644
--- a/OWNERS
+++ b/OWNERS
@@ -5,6 +5,7 @@
 rlagos@google.com
 stenning@google.com
 yizheng@google.com
+robertoalexis@google.com
 
 # TLMs
 johnchoi@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 38f9800..a810f8e 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,7 +1,8 @@
 [Hook Scripts]
 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/androidx-room/Android.mk b/androidx-room/Android.mk
index e9d215c..675dac8 100644
--- a/androidx-room/Android.mk
+++ b/androidx-room/Android.mk
@@ -36,6 +36,7 @@
     car-apache-commons-codec-nodeps:$(COMMON_LIBS_PATH)/org/eclipse/tycho/tycho-bundles-external/0.18.1/eclipse/plugins/org.apache.commons.codec_1.4.0.v201209201156.jar \
     car-auto-common-nodeps:$(COMMON_LIBS_PATH)/com/google/auto/auto-common/0.9/auto-common-0.9.jar \
     car-javapoet-nodeps:$(COMMON_LIBS_PATH)/com/squareup/javapoet/1.8.0/javapoet-1.8.0.jar \
+    car-jetbrains-annotations-nodeps:$(COMMON_LIBS_PATH)/org/jetbrains/annotations/13.0/annotations-13.0.jar \
     car-kotlin-metadata-nodeps:$(COMMON_LIBS_PATH)/me/eugeniomarletti/kotlin-metadata/1.2.1/kotlin-metadata-1.2.1.jar \
     car-sqlite-jdbc-nodeps:$(COMMON_LIBS_PATH)/org/xerial/sqlite-jdbc/3.20.1/sqlite-jdbc-3.20.1.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/drawable/background_image_scrim.xml b/car-apps-common/res/drawable/background_image_scrim.xml
index c344a6e..89a8092 100644
--- a/car-apps-common/res/drawable/background_image_scrim.xml
+++ b/car-apps-common/res/drawable/background_image_scrim.xml
@@ -28,6 +28,7 @@
     <item>
         <shape>
             <gradient
+                android:angle="0"
                 android:centerX="0.48"
                 android:startColor="@android:color/transparent"
                 android:centerColor="@android:color/black"
@@ -37,6 +38,7 @@
     <item>
         <shape>
             <gradient
+                android:angle="0"
                 android:centerX="0.38"
                 android:startColor="@android:color/transparent"
                 android:centerColor="#B3000000"
diff --git a/car-apps-common/res/drawable/hero_button_background.xml b/car-apps-common/res/drawable/hero_button_background.xml
new file mode 100644
index 0000000..15e9b2a
--- /dev/null
+++ b/car-apps-common/res/drawable/hero_button_background.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+        android:color="@*android:color/car_card_ripple_background">
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/hero_button_background_color" />
+            <corners android:radius="@dimen/hero_button_corner_radius"/>
+        </shape>
+    </item>
+</ripple>
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/control_bar_slot.xml b/car-apps-common/res/layout/control_bar_slot.xml
index bc09eed..41ef13e 100644
--- a/car-apps-common/res/layout/control_bar_slot.xml
+++ b/car-apps-common/res/layout/control_bar_slot.xml
@@ -15,8 +15,8 @@
 -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/control_bar_button_slot_size"
-    android:layout_height="@dimen/control_bar_button_slot_size"
+    android:layout_width="@dimen/control_bar_button_slot_width"
+    android:layout_height="@dimen/control_bar_button_slot_height"
     android:layout_gravity="center_vertical"
     android:visibility="visible"
     android:foregroundGravity="center"
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-media-common/res/values-h668dp/dimens.xml b/car-apps-common/res/values-h600dp/dimens.xml
similarity index 72%
rename from car-media-common/res/values-h668dp/dimens.xml
rename to car-apps-common/res/values-h600dp/dimens.xml
index 3ca1445..dcbea47 100644
--- a/car-media-common/res/values-h668dp/dimens.xml
+++ b/car-apps-common/res/values-h600dp/dimens.xml
@@ -15,7 +15,8 @@
   limitations under the License.
 -->
 <resources>
-    <!-- App bar -->
-    <!-- The height of app bar when it expends to 2 rows. Equals 2 * @dimen/appbar_first_row_height. -->
-    <dimen name="appbar_2_rows_height">192dp</dimen>
+    <dimen name="control_bar_button_background_radius">48dp</dimen>
+    <dimen name="control_bar_button_size">96dp</dimen>
+    <dimen name="control_bar_button_padding">26dp</dimen>
+    <dimen name="minimized_control_bar_button_size">96dp</dimen>
 </resources>
diff --git a/car-apps-common/res/values-night/colors.xml b/car-apps-common/res/values-night/colors.xml
index 7625452..6a2f4cf 100644
--- a/car-apps-common/res/values-night/colors.xml
+++ b/car-apps-common/res/values-night/colors.xml
@@ -22,7 +22,7 @@
     <color name="minimized_control_bar_background_color">#E00E1013</color>
 
     <color name="primary_text_color">#E0FFFFFF</color>
-    <color name="secondary_text_color">#B7FFFFFF</color>
+    <color name="secondary_text_color">#99FFFFFF</color>
 
     <color name="primary_app_icon_color">#E0FFFFFF</color>
     <color name="secondary_app_icon_color">#99FFFFFF</color>
diff --git a/car-media-common/res/values-h668dp/dimens.xml b/car-apps-common/res/values-port/styles.xml
similarity index 67%
copy from car-media-common/res/values-h668dp/dimens.xml
copy to car-apps-common/res/values-port/styles.xml
index 3ca1445..15245c1 100644
--- a/car-media-common/res/values-h668dp/dimens.xml
+++ b/car-apps-common/res/values-port/styles.xml
@@ -15,7 +15,10 @@
   limitations under the License.
 -->
 <resources>
-    <!-- App bar -->
-    <!-- The height of app bar when it expends to 2 rows. Equals 2 * @dimen/appbar_first_row_height. -->
-    <dimen name="appbar_2_rows_height">192dp</dimen>
+    <style name="FullScreenErrorMessageStyle">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:textAppearance">@style/TextAppearance.Body1</item>
+        <item name="android:gravity">center</item>
+    </style>
 </resources>
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/colors.xml b/car-apps-common/res/values/colors.xml
index c792fcb..37a7bda 100644
--- a/car-apps-common/res/values/colors.xml
+++ b/car-apps-common/res/values/colors.xml
@@ -34,6 +34,9 @@
         <item>#757575</item>
     </array>
 
+    <color name="loading_image_placeholder_color">@*android:color/car_grey_800</color>
+    <color name="improper_image_refs_tint_color">#C8FF0000</color>
+
     <color name="control_bar_background_color">@android:color/transparent</color>
     <color name="minimized_control_bar_background_color">#F50E1013</color>
     <color name="scrim_overlay_color">#C7000000</color>
@@ -48,7 +51,7 @@
     <color name="car_tab_unselected_color_light">#90FFFFFF</color>
 
     <color name="primary_text_color">#FFFFFFFF</color>
-    <color name="secondary_text_color">#90FFFFFF</color>
+    <color name="secondary_text_color">#B8FFFFFF</color>
 
     <color name="primary_app_icon_color">#FFFFFFFF</color>
     <color name="secondary_app_icon_color">#B8FFFFFF</color>
@@ -64,4 +67,7 @@
     <color name="uxr_button_text_disabled_color">#80FFFFFF</color>
 
     <color name="control_bar_button_background_color">#66ffffff</color>
+
+    <color name="hero_button_background_color">@*android:color/car_grey_868</color>
+    <color name="hero_button_text_color">@color/uxr_button_text_color_selector</color>
 </resources>
diff --git a/car-apps-common/res/values/config.xml b/car-apps-common/res/values/config.xml
index 26b0090..f274eb8 100644
--- a/car-apps-common/res/values/config.xml
+++ b/car-apps-common/res/values/config.xml
@@ -23,4 +23,22 @@
     <string name="config_scrollBarComponent" translatable="false">
         com.android.car.apps.common.widget.CarScrollBar
     </string>
+
+
+    <!-- This value must remain false for production builds. It is intended to be overlaid in
+        the simulator, so third party app developers can notice quickly that they are sending
+        improper image references.
+        See: com.android.car.apps.common.CommonFlags#flagImproperImageRefs
+    -->
+    <bool name="flag_improper_image_references">false</bool>
+
+    <string name="config_letter_tile_font_family" translatable="false">sans-serif-light</string>
+    <!-- Typeface.NORMAL=0; Typeface.BOLD=1; Typeface.ITALIC=2; Typeface.BOLD_ITALIC=3-->
+    <integer name="config_letter_tile_text_style">0</integer>
+
+    <!-- This value will determine how many letters to show in a letter tile drawable for
+         the contacts that don't have avatars. The value can be 2 (show initials),
+         1 (show one letter) or 0 (show avatar anonymous icon)
+    -->
+    <integer name="config_number_of_letters_shown_for_avatar">1</integer>
 </resources>
diff --git a/car-apps-common/res/values/dimens.xml b/car-apps-common/res/values/dimens.xml
index b2993e3..b1a51f5 100644
--- a/car-apps-common/res/values/dimens.xml
+++ b/car-apps-common/res/values/dimens.xml
@@ -27,10 +27,11 @@
     <dimen name="control_bar_margin_x">@*android:dimen/car_margin</dimen>
     <dimen name="control_bar_margin_bottom">@*android:dimen/car_padding_2</dimen>
     <dimen name="control_bar_button_size">76dp</dimen>
-    <dimen name="control_bar_button_slot_size">@dimen/control_bar_height</dimen>
+    <dimen name="control_bar_button_slot_height">@dimen/control_bar_height</dimen>
+    <dimen name="control_bar_button_slot_width">@dimen/control_bar_button_size</dimen>
     <dimen name="control_bar_elevation">0dp</dimen>
     <dimen name="control_bar_button_padding">16dp</dimen>
-    <dimen name="control_bar_button_background_radius">48dp</dimen>
+    <dimen name="control_bar_button_background_radius">38dp</dimen>
 
     <!-- Overflow button in control Bar -->
     <dimen name="overflow_button_icon_size">44dp</dimen>
@@ -76,4 +77,11 @@
         The computed blur radius is capped at 25 pixels. -->
     <item name="background_bitmap_blur_percent" format="float" type="dimen">0.05</item>
 
+    <!-- Dialog and button -->
+    <dimen name="dialog_max_width">706dp</dimen>
+    <dimen name="hero_button_max_width">@dimen/dialog_max_width</dimen>
+    <dimen name="hero_button_min_width">@dimen/touch_target_size</dimen>
+    <dimen name="hero_button_height">@dimen/touch_target_size</dimen>
+    <dimen name="hero_button_corner_radius">38dp</dimen>
+
 </resources>
diff --git a/car-apps-common/res/values/integers.xml b/car-apps-common/res/values/integers.xml
index 8367bee..bd93714 100644
--- a/car-apps-common/res/values/integers.xml
+++ b/car-apps-common/res/values/integers.xml
@@ -25,4 +25,20 @@
 
     <!-- The amount of time it takes for a new image in a CrossfadeImageView to fade in. -->
     <integer name="crossfade_image_view_fade_in_duration">250</integer>
+
+    <!-- The maximum number of thread pools used to fetch images. Applications don't share
+        pools to prevent one bad app from starving others. -->
+    <integer name="image_fetcher_thread_pools_max_count">5</integer>
+
+    <!-- The number of threads in each pool used to fetch images. -->
+    <integer name="image_fetcher_thread_pool_size">3</integer>
+
+    <!-- The amount of memory (in megabytes) LocalImageFetcher allocates to caching bitmaps
+        (and drawables) in memory. -->
+    <integer name="bitmap_memory_cache_max_size_mb">5</integer>
+
+    <!-- When caching drawables in memory, if they are not BitmapDrawable, we estimate the amount
+        of memory they take by considering they are equivalent to a 256x256 px bitmap. -->
+    <integer name="drawable_default_weight_kb">250</integer>
+
 </resources>
diff --git a/car-apps-common/res/values/styles.xml b/car-apps-common/res/values/styles.xml
index 85a5b85..6983bdc 100644
--- a/car-apps-common/res/values/styles.xml
+++ b/car-apps-common/res/values/styles.xml
@@ -14,11 +14,13 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<resources
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
     <!-- ================ -->
     <!-- ActionBar Themes -->
     <!-- ================ -->
-    <eat-comment />
+    <eat-comment/>
 
     <!-- Styles for action buttons -->
     <style name="Widget.ActionButton" parent="android:Widget.DeviceDefault.Button">
@@ -42,6 +44,13 @@
         <item name="android:layout_height">wrap_content</item>
         <item name="android:singleLine">true</item>
         <item name="android:gravity">center</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textFontWeight">400</item>
+    </style>
+
+    <style name="CarTabSelectedTextTypeface">
+        <item name="android:textFontWeight">500</item>
+        <item name="android:textStyle">normal</item>
     </style>
 
     <style name="CarTabItemIcon">
@@ -92,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">
@@ -113,4 +126,27 @@
     </style>
     <style name="PagedRecyclerView.NestedRecyclerView">
     </style>
+
+    <style name="FullScreenErrorMessageStyle">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:textAppearance">@style/TextAppearance.Body1</item>
+        <item name="android:gravity">center</item>
+        <item name="android:maxWidth">@dimen/dialog_max_width</item>
+    </style>
+
+    <style name="FullScreenErrorButtonStyle">
+        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_height">@dimen/hero_button_height</item>
+        <item name="android:textAppearance">@style/TextAppearance.Body1</item>
+        <item name="android:textStyle">bold</item>
+        <item name="android:maxWidth">@dimen/hero_button_max_width</item>
+        <item name="android:minWidth">@dimen/hero_button_min_width</item>
+        <item name="android:textAllCaps">false</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:background">@drawable/hero_button_background</item>
+        <item name="android:textColor">@color/hero_button_text_color</item>
+        <item name="android:gravity">center</item>
+        <item name="android:paddingHorizontal">@dimen/hero_button_corner_radius</item>
+    </style>
 </resources>
diff --git a/car-apps-common/src/com/android/car/apps/common/AccountImageChangeObserver.java b/car-apps-common/src/com/android/car/apps/common/AccountImageChangeObserver.java
deleted file mode 100644
index cbfe5a4..0000000
--- a/car-apps-common/src/com/android/car/apps/common/AccountImageChangeObserver.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import static android.Manifest.permission.GET_ACCOUNTS;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.annotation.RequiresPermission;
-import android.content.ContentUris;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.Contacts;
-import android.text.TextUtils;
-
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-
-/**
- * @hide
- */
-public class AccountImageChangeObserver {
-    private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
-
-    private static final Object sObserverInstanceLock = new Object();
-    private static AccountImageChangeObserver sObserver;
-
-    private class ContactChangeContentObserver extends ContentObserver {
-        private final Account mWatchedAccount;
-        private final LinkedHashSet<Uri> mUrisToNotify;
-        private final Object mLock = new Object();
-        private final Context mContext;
-        private String mCurrentImageUri;
-
-        public ContactChangeContentObserver(Context context, Account watchedAccount) {
-            super(null);
-            mWatchedAccount = watchedAccount;
-            mUrisToNotify = new LinkedHashSet<>();
-            mContext = context;
-            mCurrentImageUri = AccountImageHelper.getAccountPictureUri(mContext, mWatchedAccount);
-        }
-
-        @Override
-        public boolean deliverSelfNotifications() {
-            return true;
-        }
-
-        public void addUriToNotifyList(Uri uri) {
-            synchronized (mLock) {
-                mUrisToNotify.add(uri);
-            }
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            String newUri = AccountImageHelper.getAccountPictureUri(mContext, mWatchedAccount);
-
-            if (TextUtils.equals(mCurrentImageUri, newUri)) {
-                // no change, no need to notify
-                return;
-            }
-
-            synchronized (mLock) {
-                for (Uri uri : mUrisToNotify) {
-                    mContext.getContentResolver().notifyChange(uri, null);
-                }
-
-                mCurrentImageUri = newUri;
-            }
-        }
-    }
-
-    private final HashMap<String, ContactChangeContentObserver> mObserverMap;
-
-
-    /**
-     * get the singleton AccountImageChangeObserver for the application
-     */
-    public final static AccountImageChangeObserver getInstance() {
-        if (sObserver == null) {
-            synchronized (sObserverInstanceLock) {
-                if (sObserver == null) {
-                    sObserver = new AccountImageChangeObserver();
-                }
-            }
-        }
-        return sObserver;
-    }
-
-    public AccountImageChangeObserver() {
-        mObserverMap = new HashMap<>();
-    }
-
-    @RequiresPermission(GET_ACCOUNTS)
-    public synchronized void registerChangeUriIfPresent(BitmapWorkerOptions options) {
-        Uri imageUri = options.getResourceUri();
-        // Only register URIs that match the Account Image URI schema, and
-        // have a change notify URI specified.
-        if (imageUri != null && UriUtils.isAccountImageUri(imageUri)) {
-            Uri changeNotifUri = UriUtils.getAccountImageChangeNotifyUri(imageUri);
-            imageUri = imageUri.buildUpon().clearQuery().build();
-
-            if (changeNotifUri == null) {
-                // No change Notiy URI specified
-                return;
-            }
-
-            String accountName = UriUtils.getAccountName(imageUri);
-            Context context = options.getContext();
-
-            if (accountName != null && context != null) {
-                Account thisAccount = null;
-                for (Account account : AccountManager.get(context).
-                        getAccountsByType(GOOGLE_ACCOUNT_TYPE)) {
-                    if (account.name.equals(accountName)) {
-                        thisAccount = account;
-                        break;
-                    }
-                }
-                if (thisAccount != null) {
-                    ContactChangeContentObserver observer;
-
-                    if (mObserverMap.containsKey(thisAccount.name)) {
-                        observer = mObserverMap.get(thisAccount.name);
-                        if (observer != null) {
-                            observer.addUriToNotifyList(changeNotifUri);
-                        }
-                    } else {
-                        long contactId = getContactIdForAccount(context, thisAccount);
-                        if (contactId != -1) {
-                            Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
-                                    contactId);
-                            observer = new ContactChangeContentObserver(context, thisAccount);
-                            mObserverMap.put(thisAccount.name, observer);
-                            observer.addUriToNotifyList(changeNotifUri);
-                            context.getContentResolver().registerContentObserver(contactUri, false,
-                                    observer);
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    private long getContactIdForAccount(Context context, Account account) {
-        // Look up this account in the contacts database.
-        String[] projection = new String[] {
-                ContactsContract.Data._ID,
-                ContactsContract.Data.CONTACT_ID,
-                ContactsContract.Data.LOOKUP_KEY
-        };
-        String selection =
-                ContactsContract.CommonDataKinds.Email.ADDRESS + " LIKE ?";
-        String[] selectionArgs = new String[] { account.name };
-        Cursor c = null;
-        long contactId = -1;
-        String lookupKey = null;
-        try {
-            c = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
-                    projection, selection, selectionArgs, null);
-            if (c.moveToNext()) {
-                contactId = c.getLong(1);
-                lookupKey = c.getString(2);
-            }
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-
-        if (contactId != -1 && !TextUtils.isEmpty(lookupKey)) {
-            return contactId;
-        }
-
-        return -1;
-    }
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/BackgroundImageView.java b/car-apps-common/src/com/android/car/apps/common/BackgroundImageView.java
index 954b82a..043f41a 100644
--- a/car-apps-common/src/com/android/car/apps/common/BackgroundImageView.java
+++ b/car-apps-common/src/com/android/car/apps/common/BackgroundImageView.java
@@ -18,7 +18,10 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
+import android.util.Size;
 import android.view.View;
 
 import androidx.annotation.Nullable;
@@ -33,7 +36,7 @@
     private CrossfadeImageView mImageView;
 
     /** Configuration (controlled from resources) */
-    private int mBitmapTargetSize;
+    private Size mBitmapTargetSize;
     private float mBitmapBlurPercent;
 
     private View mDarkeningScrim;
@@ -54,7 +57,8 @@
         mImageView = findViewById(R.id.background_image_image);
         mDarkeningScrim = findViewById(R.id.background_image_darkening_scrim);
 
-        mBitmapTargetSize = getResources().getInteger(R.integer.background_bitmap_target_size_px);
+        int size = getResources().getInteger(R.integer.background_bitmap_target_size_px);
+        mBitmapTargetSize = new Size(size, size);
         mBitmapBlurPercent = getResources().getFloat(R.dimen.background_bitmap_blur_percent);
 
         TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
@@ -69,19 +73,40 @@
     }
 
     /**
+     * @deprecated Use {@link #setBackgroundDrawable} instead, and make sure to only call when the
+     * image is actually different! TODO(b/139387273).
      * Sets the image to display to a bitmap
      * @param bitmap The image to show. It will be scaled to the correct size and blurred.
      * @param showAnimation Whether or not to cross fade to the new image
      */
+    @Deprecated
     public void setBackgroundImage(@Nullable Bitmap bitmap, boolean showAnimation) {
-        if (bitmap == null) {
+        Drawable drawable = (bitmap != null) ? new BitmapDrawable(bitmap) : null;
+        updateBlur(drawable, showAnimation);
+    }
+
+    /** Sets the drawable that will be displayed blurred by this view. */
+    public void setBackgroundDrawable(@Nullable Drawable drawable) {
+        setBackgroundDrawable(drawable, true);
+    }
+
+    /**
+     * Sets the drawable that will be displayed blurred by this view specifying if animation is
+     * enabled.
+     */
+    public void setBackgroundDrawable(@Nullable Drawable drawable, boolean showAnimation) {
+        updateBlur(drawable, showAnimation);
+    }
+
+    private void updateBlur(@Nullable Drawable drawable, boolean showAnimation) {
+        if (drawable == null) {
             mImageView.setImageBitmap(null, false);
             return;
         }
 
-        bitmap = ImageUtils.blur(getContext(), bitmap, mBitmapTargetSize, mBitmapBlurPercent);
-        mImageView.setImageBitmap(bitmap, showAnimation);
-
+        Bitmap src = BitmapUtils.fromDrawable(drawable, mBitmapTargetSize);
+        Bitmap blurred = ImageUtils.blur(getContext(), src, mBitmapTargetSize, mBitmapBlurPercent);
+        mImageView.setImageBitmap(blurred, showAnimation);
         invalidate();
         requestLayout();
     }
@@ -91,14 +116,6 @@
         mImageView.setBackgroundColor(color);
     }
 
-    /**
-     * Gets the desired size for an image to pass to {@link #setBackgroundImage}. That size is
-     * defined by R.integer.background_bitmap_target_size_px.
-     */
-    public int getDesiredBackgroundSize() {
-        return mBitmapTargetSize;
-    }
-
     /** Dims/undims the background image by 30% */
     public void setDimmed(boolean dim) {
         mDarkeningScrim.setVisibility(dim ? View.VISIBLE : View.GONE);
diff --git a/car-apps-common/src/com/android/car/apps/common/BitmapDownloader.java b/car-apps-common/src/com/android/car/apps/common/BitmapDownloader.java
deleted file mode 100644
index 9600585..0000000
--- a/car-apps-common/src/com/android/car/apps/common/BitmapDownloader.java
+++ /dev/null
@@ -1,338 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.util.Log;
-import android.util.LruCache;
-import android.widget.ImageView;
-
-import java.lang.ref.SoftReference;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * Downloader class which loads a resource URI into an image view.
- * <p>
- * This class adds a cache over BitmapWorkerTask.
- */
-public class BitmapDownloader {
-
-    private static final String TAG = "BitmapDownloader";
-
-    private static final boolean DEBUG = false;
-
-    private static final int CORE_POOL_SIZE = 5;
-
-    private static final Executor BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR =
-            Executors.newFixedThreadPool(CORE_POOL_SIZE);
-
-    // 1/4 of max memory is used for bitmap mem cache
-    private static final int MEM_TO_CACHE = 4;
-
-    // hard limit for bitmap mem cache in MB
-    private static final int CACHE_HARD_LIMIT = 32;
-
-    /**
-     * bitmap cache item structure saved in LruCache
-     */
-    private static class BitmapItem {
-        /**
-         * cached bitmap
-         */
-        Bitmap mBitmap;
-        /**
-         * indicate if the bitmap is scaled down from original source (never scale up)
-         */
-        boolean mScaled;
-
-        public BitmapItem(Bitmap bitmap, boolean scaled) {
-            mBitmap = bitmap;
-            mScaled = scaled;
-        }
-    }
-
-    private LruCache<String, BitmapItem> mMemoryCache;
-
-    private static BitmapDownloader sBitmapDownloader;
-
-    private static final Object sBitmapDownloaderLock = new Object();
-
-    // Bitmap cache also uses size of Bitmap as part of key.
-    // Bitmap cache is divided into following buckets by height:
-    // TODO: Pano currently is caring more about height, what about width in key?
-    // height <= 128, 128 < height <= 512, height > 512
-    // Different bitmap cache buckets save different bitmap cache items.
-    // Bitmaps within same bucket share the largest cache item.
-    private static final int[] SIZE_BUCKET = new int[]{128, 512, Integer.MAX_VALUE};
-
-    public static abstract class BitmapCallback {
-        SoftReference<BitmapWorkerTask> mTask;
-
-        public abstract void onBitmapRetrieved(Bitmap bitmap);
-    }
-
-    /**
-     * get the singleton BitmapDownloader for the application
-     */
-    public final static BitmapDownloader getInstance(Context context) {
-        if (sBitmapDownloader == null) {
-            synchronized(sBitmapDownloaderLock) {
-                if (sBitmapDownloader == null) {
-                    sBitmapDownloader = new BitmapDownloader(context);
-                }
-            }
-        }
-        return sBitmapDownloader;
-    }
-
-    public BitmapDownloader(Context context) {
-        int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE))
-                .getMemoryClass();
-        memClass = memClass / MEM_TO_CACHE;
-        if (memClass > CACHE_HARD_LIMIT) {
-            memClass = CACHE_HARD_LIMIT;
-        }
-        int cacheSize = 1024 * 1024 * memClass;
-        mMemoryCache = new LruCache<String, BitmapItem>(cacheSize) {
-            @Override
-            protected int sizeOf(String key, BitmapItem bitmap) {
-                return bitmap.mBitmap.getByteCount();
-            }
-        };
-    }
-
-    /**
-     * load bitmap in current thread, will *block* current thread.
-     * @deprecated
-     */
-    @Deprecated
-    public final Bitmap loadBitmapBlocking(final BitmapWorkerOptions options) {
-        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
-        Bitmap bitmap = null;
-        if (hasAccountImageUri) {
-            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
-        } else {
-            bitmap = getBitmapFromMemCache(options);
-        }
-
-        if (bitmap == null) {
-            BitmapWorkerTask task = new BitmapWorkerTask(null) {
-                @Override
-                protected Bitmap doInBackground(BitmapWorkerOptions... params) {
-                    final Bitmap bitmap = super.doInBackground(params);
-                    if (bitmap != null && !hasAccountImageUri) {
-                        addBitmapToMemoryCache(params[0], bitmap, isScaled());
-                    }
-                    return bitmap;
-                }
-            };
-
-            return task.doInBackground(options);
-        }
-        return bitmap;
-    }
-
-    /**
-     * Loads the bitmap into the image view.
-     */
-    public void loadBitmap(final BitmapWorkerOptions options, final ImageView imageView) {
-        cancelDownload(imageView);
-        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
-        Bitmap bitmap = null;
-        if (hasAccountImageUri) {
-            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
-        } else {
-            bitmap = getBitmapFromMemCache(options);
-        }
-
-        if (bitmap != null) {
-            imageView.setImageBitmap(bitmap);
-        } else {
-            BitmapWorkerTask task = new BitmapWorkerTask(imageView) {
-                @Override
-                protected Bitmap doInBackground(BitmapWorkerOptions... params) {
-                    Bitmap bitmap = super.doInBackground(params);
-                    if (bitmap != null && !hasAccountImageUri) {
-                        addBitmapToMemoryCache(params[0], bitmap, isScaled());
-                    }
-                    return bitmap;
-                }
-            };
-            imageView.setTag(R.id.imageDownloadTask, new SoftReference<BitmapWorkerTask>(task));
-            task.execute(options);
-        }
-    }
-
-    /**
-     * Loads the bitmap.
-     * <p>
-     * This will be sent back to the callback object.
-     */
-    public void getBitmap(final BitmapWorkerOptions options, final BitmapCallback callback) {
-        cancelDownload(callback);
-        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
-        final Bitmap bitmap = hasAccountImageUri ? null : getBitmapFromMemCache(options);
-        if (hasAccountImageUri) {
-            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
-        }
-
-        BitmapWorkerTask task = new BitmapWorkerTask(null) {
-            @Override
-            protected Bitmap doInBackground(BitmapWorkerOptions... params) {
-                if (bitmap != null) {
-                    return bitmap;
-                }
-                final Bitmap bitmap = super.doInBackground(params);
-                if (bitmap != null && !hasAccountImageUri) {
-                    addBitmapToMemoryCache(params[0], bitmap, isScaled());
-                }
-                return bitmap;
-            }
-
-            @Override
-            protected void onPostExecute(Bitmap bitmap) {
-                callback.onBitmapRetrieved(bitmap);
-            }
-        };
-        callback.mTask = new SoftReference<BitmapWorkerTask>(task);
-        task.executeOnExecutor(BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
-    }
-
-    /**
-     * Cancel download<p>
-     * @param key {@link BitmapCallback} or {@link ImageView}
-     */
-    @SuppressWarnings("unchecked")
-    public boolean cancelDownload(Object key) {
-        BitmapWorkerTask task = null;
-        if (key instanceof ImageView) {
-            ImageView imageView = (ImageView)key;
-            SoftReference<BitmapWorkerTask> softReference =
-                    (SoftReference<BitmapWorkerTask>) imageView.getTag(R.id.imageDownloadTask);
-            if (softReference != null) {
-                task = softReference.get();
-                softReference.clear();
-            }
-        } else if (key instanceof BitmapCallback) {
-            BitmapCallback callback = (BitmapCallback) key;
-            if (callback.mTask != null) {
-                task = callback.mTask.get();
-                callback.mTask = null;
-            }
-        }
-        if (task != null) {
-            return task.cancel(true);
-        }
-        return false;
-    }
-
-    private static String getBucketKey(String baseKey, Bitmap.Config bitmapConfig, int width) {
-        for (int i = 0; i < SIZE_BUCKET.length; i++) {
-            if (width <= SIZE_BUCKET[i]) {
-                return new StringBuilder(baseKey.length() + 16).append(baseKey)
-                        .append(":").append(bitmapConfig == null ? "" : bitmapConfig.ordinal())
-                        .append(":").append(SIZE_BUCKET[i]).toString();
-            }
-        }
-        // should never happen because last bucket is Integer.MAX_VALUE
-        throw new RuntimeException();
-    }
-
-    private void addBitmapToMemoryCache(BitmapWorkerOptions key, Bitmap bitmap, boolean isScaled) {
-        if (!key.isMemCacheEnabled()) {
-            return;
-        }
-        String bucketKey = getBucketKey(
-                key.getCacheKey(), key.getBitmapConfig(), bitmap.getHeight());
-        BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
-        if (bitmapItem != null) {
-            Bitmap currentBitmap = bitmapItem.mBitmap;
-            // If somebody else happened to get a larger one in the bucket, discard our bitmap.
-            // TODO: need a better way to prevent current downloading for the same Bitmap
-            if (currentBitmap.getWidth() >= bitmap.getWidth() && currentBitmap.getHeight()
-                    >= bitmap.getHeight()) {
-                return;
-            }
-        }
-        if (DEBUG) {
-            Log.d(TAG, "add cache "+bucketKey+" isScaled = "+isScaled);
-        }
-        bitmapItem = new BitmapItem(bitmap, isScaled);
-        mMemoryCache.put(bucketKey, bitmapItem);
-    }
-
-    private Bitmap getBitmapFromMemCache(BitmapWorkerOptions key) {
-        if (key.getHeight() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
-            // 1. find the bitmap in the size bucket
-            String bucketKey =
-                    getBucketKey(key.getCacheKey(), key.getBitmapConfig(), key.getHeight());
-            BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
-            if (bitmapItem != null) {
-                Bitmap bitmap = bitmapItem.mBitmap;
-                // now we have the bitmap in the bucket, use it when the bitmap is not scaled or
-                // if the size is larger than or equals to the output size
-                if (!bitmapItem.mScaled) {
-                    return bitmap;
-                }
-                if (bitmap.getHeight() >= key.getHeight()) {
-                    return bitmap;
-                }
-            }
-            // 2. find un-scaled bitmap in smaller buckets.  If the un-scaled bitmap exists
-            // in higher buckets,  we still need to scale it down.  Right now we just
-            // return null and let the BitmapWorkerTask to do the same job again.
-            // TODO: use the existing unscaled bitmap and we don't need to load it from resource
-            // or network again.
-            for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
-                if (SIZE_BUCKET[i] >= key.getHeight()) {
-                    continue;
-                }
-                bucketKey = getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
-                bitmapItem = mMemoryCache.get(bucketKey);
-                if (bitmapItem != null && !bitmapItem.mScaled) {
-                    return bitmapItem.mBitmap;
-                }
-            }
-            return null;
-        }
-        // 3. find un-scaled bitmap if size is not specified
-        for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
-            String bucketKey =
-                    getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
-            BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
-            if (bitmapItem != null && !bitmapItem.mScaled) {
-                return bitmapItem.mBitmap;
-            }
-        }
-        return null;
-    }
-
-    public Bitmap getLargestBitmapFromMemCache(BitmapWorkerOptions key) {
-        // find largest bitmap matching the key
-        for (int i = SIZE_BUCKET.length - 1; i >= 0; i--) {
-            String bucketKey =
-                    getBucketKey(key.getCacheKey(), key.getBitmapConfig(), SIZE_BUCKET[i]);
-            BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
-            if (bitmapItem != null) {
-                return bitmapItem.mBitmap;
-            }
-        }
-        return null;
-    }
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/BitmapUtils.java b/car-apps-common/src/com/android/car/apps/common/BitmapUtils.java
index 0414073..08b5dd1 100644
--- a/car-apps-common/src/com/android/car/apps/common/BitmapUtils.java
+++ b/car-apps-common/src/com/android/car/apps/common/BitmapUtils.java
@@ -15,8 +15,22 @@
  */
 package com.android.car.apps.common;
 
+import static android.graphics.Bitmap.Config.ARGB_8888;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.util.Log;
+import android.util.Size;
 
 public class BitmapUtils {
     private static final String TAG = "BitmapUtils";
@@ -93,4 +107,64 @@
         int y =(bm.getHeight() - height) / 2;
         return Bitmap.createBitmap(bm, x, y, width, height);
     }
+
+    /** Creates a copy of the given bitmap and applies the given color over the result */
+    @Nullable
+    public static Bitmap createTintedBitmap(@Nullable Bitmap image, @ColorInt int colorOverlay) {
+        if (image == null) return null;
+        Bitmap clone = Bitmap.createBitmap(image.getWidth(), image.getHeight(), ARGB_8888);
+        Canvas canvas = new Canvas(clone);
+        canvas.drawBitmap(image, 0f, 0f, new Paint());
+        canvas.drawColor(colorOverlay);
+        return clone;
+    }
+
+    /** Returns a tinted drawable if flagging is enabled and the given drawable is a bitmap. */
+    @NonNull
+    public static Drawable maybeFlagDrawable(@NonNull Context context, @NonNull Drawable drawable) {
+        if (drawable instanceof BitmapDrawable) {
+            CommonFlags flags = CommonFlags.getInstance(context);
+            Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
+            if (flags.shouldFlagImproperImageRefs() && bitmap != null) {
+                Resources res = context.getResources();
+                int tint = context.getColor(R.color.improper_image_refs_tint_color);
+                drawable = new BitmapDrawable(res, BitmapUtils.createTintedBitmap(bitmap, tint));
+            }
+        }
+        return drawable;
+    }
+
+    /** Renders the drawable into a bitmap if needed. */
+    public static Bitmap fromDrawable(Drawable drawable, @Nullable Size bitmapSize) {
+        if (drawable instanceof BitmapDrawable) {
+            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+            if (bitmapDrawable.getBitmap() != null) {
+                return bitmapDrawable.getBitmap();
+            }
+        }
+
+        Matrix matrix = new Matrix();
+        if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+            bitmapSize = new Size(1, 1);
+            drawable.setBounds(0, 0, bitmapSize.getWidth(), bitmapSize.getHeight());
+        } else {
+            if (bitmapSize == null) {
+                bitmapSize = new Size(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+            }
+            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+            RectF srcR = new RectF(0f, 0f, drawable.getIntrinsicWidth(),
+                    drawable.getIntrinsicHeight());
+            RectF dstR = new RectF(0f, 0f, bitmapSize.getWidth(), bitmapSize.getHeight());
+            matrix.setRectToRect(srcR, dstR, Matrix.ScaleToFit.CENTER);
+        }
+
+        Bitmap bitmap = Bitmap.createBitmap(bitmapSize.getWidth(), bitmapSize.getHeight(),
+                Bitmap.Config.ARGB_8888);
+
+        Canvas canvas = new Canvas(bitmap);
+        canvas.setMatrix(matrix);
+        drawable.draw(canvas);
+
+        return bitmap;
+    }
 }
diff --git a/car-apps-common/src/com/android/car/apps/common/BitmapWorkerOptions.java b/car-apps-common/src/com/android/car/apps/common/BitmapWorkerOptions.java
deleted file mode 100644
index 5f22e79..0000000
--- a/car-apps-common/src/com/android/car/apps/common/BitmapWorkerOptions.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import android.content.Context;
-import android.content.Intent.ShortcutIconResource;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.text.TextUtils;
-
-/**
- * Options for loading bitmap resources from different sources and for scaling to an appropriate
- * resolution.
- */
-public class BitmapWorkerOptions {
-
-    /** Max image size handled by android.graphics */
-    static final int MAX_IMAGE_DIMENSION_PX = 2048;
-
-    /** flag to force disable memory cache */
-    public static final int CACHE_FLAG_MEM_DISABLED = 1;
-    /** TODO support disk cache options */
-    public static final int CACHE_FLAG_DISK_DISABLED = 2;
-
-    private ShortcutIconResource mIconResource;
-    private Uri mResourceUri;
-
-    private int mWidth;
-    private int mHeight;
-    private Context mContext;
-    private int mCacheFlag;
-    private Bitmap.Config mBitmapConfig;
-
-    private String mKey;
-
-    /**
-     * Builds options for a bitmap worker task.
-     */
-    public static class Builder {
-
-        private String mPackageName;
-        private String mResourceName;
-        private Uri mResourceUri;
-
-        private int mWidth;
-        private int mHeight;
-        private Context mContext;
-        private int mCacheFlag;
-        private Bitmap.Config mBitmapConfig;
-
-        public Builder(Context context) {
-            mWidth = MAX_IMAGE_DIMENSION_PX;
-            mHeight = MAX_IMAGE_DIMENSION_PX;
-            mContext = context;
-            mCacheFlag = 0;
-            mBitmapConfig = null;
-        }
-
-        public BitmapWorkerOptions build() {
-            BitmapWorkerOptions options = new BitmapWorkerOptions();
-
-            if (!TextUtils.isEmpty(mPackageName)) {
-                options.mIconResource = new ShortcutIconResource();
-                options.mIconResource.packageName = mPackageName;
-                options.mIconResource.resourceName = mResourceName;
-            }
-
-            final int largestDim = Math.max(mWidth, mHeight);
-            if (largestDim > MAX_IMAGE_DIMENSION_PX) {
-                double scale = (double) MAX_IMAGE_DIMENSION_PX / largestDim;
-                mWidth *= scale;
-                mHeight *= scale;
-            }
-
-            options.mResourceUri = mResourceUri;
-            options.mWidth = mWidth;
-            options.mHeight = mHeight;
-            options.mContext = mContext;
-            options.mCacheFlag = mCacheFlag;
-            options.mBitmapConfig = mBitmapConfig;
-            if (options.mIconResource == null && options.mResourceUri == null) {
-                throw new RuntimeException("Both Icon and ResourceUri are null");
-            }
-            return options;
-        }
-
-        public Builder resource(String packageName, String resourceName) {
-            mPackageName = packageName;
-            mResourceName = resourceName;
-            return this;
-        }
-
-        public Builder resource(ShortcutIconResource iconResource) {
-            mPackageName = iconResource.packageName;
-            mResourceName = iconResource.resourceName;
-            return this;
-        }
-
-        public Builder resource(Uri resourceUri) {
-            mResourceUri = resourceUri;
-            return this;
-        }
-
-        public Builder width(int width) {
-            if (width > 0) {
-                mWidth = width;
-            } else {
-                throw new IllegalArgumentException("Can't set width to " + width);
-            }
-            return this;
-        }
-
-        public Builder height(int height) {
-            if (height > 0) {
-                mHeight = height;
-            } else {
-                throw new IllegalArgumentException("Can't set height to " + height);
-            }
-            return this;
-        }
-
-        public Builder cacheFlag(int flag) {
-            mCacheFlag = flag;
-            return this;
-        }
-
-        public Builder bitmapConfig(Bitmap.Config config) {
-            mBitmapConfig = config;
-            return this;
-        }
-
-    }
-
-    /**
-     * Private constructor.
-     * <p>
-     * Use a {@link Builder} to create.
-     */
-    private BitmapWorkerOptions() {
-    }
-
-    public ShortcutIconResource getIconResource() {
-        return mIconResource;
-    }
-
-    public Uri getResourceUri() {
-        return mResourceUri;
-    }
-
-    public int getWidth() {
-        return mWidth;
-    }
-
-    public int getHeight() {
-        return mHeight;
-    }
-
-    public Context getContext() {
-        return mContext;
-    }
-
-    public boolean isFromResource() {
-        return getIconResource() != null ||
-                UriUtils.isAndroidResourceUri(getResourceUri())
-                || UriUtils.isShortcutIconResourceUri(getResourceUri());
-    }
-
-    /**
-     * Combination of CACHE_FLAG_MEM_DISABLED and CACHE_FLAG_DISK_DISABLED,
-     * 0 for fully cache enabled
-     */
-    public int getCacheFlag() {
-        return mCacheFlag;
-    }
-
-    public boolean isMemCacheEnabled() {
-        return (mCacheFlag & CACHE_FLAG_MEM_DISABLED) == 0;
-    }
-
-    public boolean isDiskCacheEnabled() {
-        return (mCacheFlag & CACHE_FLAG_DISK_DISABLED) == 0;
-    }
-
-    /**
-     * @return  preferred Bitmap config to decode bitmap, null for auto detect.
-     * Use {@link Builder#bitmapConfig(Bitmap.Config)} to change it.
-     */
-    public Bitmap.Config getBitmapConfig() {
-        return mBitmapConfig;
-    }
-
-    public String getCacheKey() {
-        if (mKey == null) {
-            mKey = mIconResource != null ? mIconResource.packageName + "/"
-                    + mIconResource.resourceName : mResourceUri.toString();
-        }
-        return mKey;
-    }
-
-    @Override
-    public String toString() {
-        if (mIconResource == null) {
-            return "URI: " + mResourceUri;
-        } else {
-            return "PackageName: " + mIconResource.packageName + " Resource: " + mIconResource
-                    + " URI: " + mResourceUri;
-        }
-    }
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/BitmapWorkerTask.java b/car-apps-common/src/com/android/car/apps/common/BitmapWorkerTask.java
deleted file mode 100644
index aa8d67f..0000000
--- a/car-apps-common/src/com/android/car/apps/common/BitmapWorkerTask.java
+++ /dev/null
@@ -1,349 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.content.Context;
-import android.content.Intent.ShortcutIconResource;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.util.Log;
-import android.util.TypedValue;
-import android.widget.ImageView;
-
-import java.io.BufferedInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-import java.net.HttpURLConnection;
-import java.net.URL;
-
-/**
- * AsyncTask which loads a bitmap.
- * <p>
- * The source of this can be another package (via a resource), a URI (content provider), or
- * a file path.
- *
- * @see BitmapWorkerOptions
- * @hide
- */
-public class BitmapWorkerTask extends AsyncTask<BitmapWorkerOptions, Void, Bitmap> {
-
-    private static final String TAG = "BitmapWorker";
-    private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
-
-    private static final boolean DEBUG = false;
-
-    private static final int SOCKET_TIMEOUT = 10000;
-    private static final int READ_TIMEOUT = 10000;
-
-    private final WeakReference<ImageView> mImageView;
-    // a flag for if the bitmap is scaled from original source
-    protected boolean mScaled;
-
-    public BitmapWorkerTask(ImageView imageView) {
-        mImageView = new WeakReference<ImageView>(imageView);
-        mScaled = false;
-    }
-
-    @Override
-    protected Bitmap doInBackground(BitmapWorkerOptions... params) {
-
-        return retrieveBitmap(params[0]);
-    }
-
-    protected Bitmap retrieveBitmap(BitmapWorkerOptions workerOptions) {
-        try {
-            if (workerOptions.getIconResource() != null) {
-                return getBitmapFromResource(workerOptions.getContext(),
-                        workerOptions.getIconResource(),
-                        workerOptions);
-            } else if (workerOptions.getResourceUri() != null) {
-                if (UriUtils.isAndroidResourceUri(workerOptions.getResourceUri())
-                        || UriUtils.isShortcutIconResourceUri(workerOptions.getResourceUri())) {
-                    // Make an icon resource from this.
-                    return getBitmapFromResource(workerOptions.getContext(),
-                            UriUtils.getIconResource(workerOptions.getResourceUri()),
-                            workerOptions);
-                } else if (UriUtils.isWebUri(workerOptions.getResourceUri())) {
-                        return getBitmapFromHttp(workerOptions);
-                } else if (UriUtils.isContentUri(workerOptions.getResourceUri())) {
-                    return getBitmapFromContent(workerOptions);
-                } else if (UriUtils.isAccountImageUri(workerOptions.getResourceUri())) {
-                    return getAccountImage(workerOptions);
-                } else {
-                    Log.e(TAG, "Error loading bitmap - unknown resource URI! "
-                            + workerOptions.getResourceUri());
-                }
-            } else {
-                Log.e(TAG, "Error loading bitmap - no source!");
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Error loading url " + workerOptions.getResourceUri(), e);
-            return null;
-        } catch (RuntimeException e) {
-            Log.e(TAG, "Critical Error loading url " + workerOptions.getResourceUri(), e);
-            return null;
-        }
-
-        return null;
-    }
-
-    @Override
-    protected void onPostExecute(Bitmap bitmap) {
-        if (mImageView != null) {
-            final ImageView imageView = mImageView.get();
-            if (imageView != null) {
-                imageView.setImageBitmap(bitmap);
-            }
-        }
-    }
-
-    private Bitmap getBitmapFromResource(Context context, ShortcutIconResource iconResource,
-            BitmapWorkerOptions outputOptions) throws IOException {
-        if (DEBUG) {
-            Log.d(TAG, "Loading " + iconResource.toString());
-        }
-        try {
-            Object drawable = loadDrawable(context, iconResource);
-            if (drawable instanceof InputStream) {
-                // Most of these are bitmaps, so resize properly.
-                return decodeBitmap((InputStream) drawable, outputOptions);
-            } else if (drawable instanceof Drawable){
-                return createIconBitmap((Drawable) drawable, outputOptions);
-            } else {
-                Log.w(TAG, "getBitmapFromResource failed, unrecognized resource: " + drawable);
-                return null;
-            }
-        } catch (NameNotFoundException e) {
-            Log.w(TAG, "Could not load package: " + iconResource.packageName + "! NameNotFound");
-            return null;
-        } catch (NotFoundException e) {
-            Log.w(TAG, "Could not load resource: " + iconResource.resourceName + "! NotFound");
-            return null;
-        }
-    }
-
-    public final boolean isScaled() {
-        return mScaled;
-    }
-
-    private Bitmap decodeBitmap(InputStream in, BitmapWorkerOptions options)
-            throws IOException {
-        CachedInputStream bufferedStream = null;
-        BitmapFactory.Options bitmapOptions = null;
-        try {
-            bufferedStream = new CachedInputStream(in);
-            // Let the bufferedStream be able to mark unlimited bytes up to full stream length.
-            // The value that BitmapFactory uses (1024) is too small for detecting bounds
-            bufferedStream.setOverrideMarkLimit(Integer.MAX_VALUE);
-            bitmapOptions = new BitmapFactory.Options();
-            bitmapOptions.inJustDecodeBounds = true;
-            if (options.getBitmapConfig() != null) {
-                bitmapOptions.inPreferredConfig = options.getBitmapConfig();
-            }
-            bitmapOptions.inTempStorage = ByteArrayPool.get16KBPool().allocateChunk();
-            bufferedStream.mark(Integer.MAX_VALUE);
-            BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
-
-            float heightScale = (float) bitmapOptions.outHeight / options.getHeight();
-            float widthScale = (float) bitmapOptions.outWidth / options.getWidth();
-            // We take the smaller value because we will crop the result later.
-            float scale = heightScale < widthScale ? heightScale : widthScale;
-
-            bitmapOptions.inJustDecodeBounds = false;
-            if (scale >= 4) {
-                // Sampling looks really, really bad. So sample part way and then smooth
-                // the result with bilinear interpolation.
-                bitmapOptions.inSampleSize = (int) scale / 2;
-            }
-            // Reset buffer to original position and disable the overrideMarkLimit
-            bufferedStream.reset();
-            bufferedStream.setOverrideMarkLimit(0);
-
-            Bitmap bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
-            // We need to check isCancelled() and throw away the result if we are cancelled because
-            // if we're loading over HTTP, canceling an AsyncTask causes the HTTP library to throw
-            // an exception, which the bitmap library then eats and returns a partially decoded
-            // bitmap. This behavior no longer will happen in lmp-mr1.
-            if (bitmap == null || isCancelled()) {
-                return null;
-            }
-
-            Bitmap result = bitmap;
-            if (options.getWidth() < bitmap.getWidth()
-                    || options.getHeight() < bitmap.getHeight()) {
-                result = BitmapUtils.scaleBitmap(bitmap, options.getWidth(), options.getHeight());
-                mScaled = true;
-            }
-
-            if (result != bitmap) {
-                bitmap.recycle();
-            }
-            return result;
-
-        } finally {
-            if (bitmapOptions != null) {
-                ByteArrayPool.get16KBPool().releaseChunk(bitmapOptions.inTempStorage);
-            }
-            if (bufferedStream != null) {
-                bufferedStream.close();
-            }
-        }
-    }
-
-    private Bitmap getBitmapFromHttp(BitmapWorkerOptions options) throws IOException {
-        URL url = new URL(options.getResourceUri().toString());
-        if (DEBUG) {
-            Log.d(TAG, "Loading " + url);
-        }
-        HttpURLConnection connection = null;
-        try {
-            connection = (HttpURLConnection) url.openConnection();
-            connection.setConnectTimeout(SOCKET_TIMEOUT);
-            connection.setReadTimeout(READ_TIMEOUT);
-            InputStream in = new BufferedInputStream(connection.getInputStream());
-            return decodeBitmap(in, options);
-        } finally {
-            if (DEBUG) {
-                Log.d(TAG, "loading done "+url);
-            }
-            if (connection != null) {
-                connection.disconnect();
-            }
-        }
-    }
-
-    private Bitmap getBitmapFromContent(BitmapWorkerOptions options) throws IOException {
-        InputStream bitmapStream =
-                options.getContext().getContentResolver().openInputStream(options.getResourceUri());
-        if (bitmapStream != null) {
-            return decodeBitmap(bitmapStream, options);
-        } else {
-            Log.w(TAG, "Content provider returned a null InputStream when trying to " +
-                    "open resource.");
-            return null;
-        }
-    }
-
-    /**
-     * load drawable for non-bitmap resource or InputStream for bitmap resource without
-     * caching Bitmap in Resources.  So that caller can maintain a different caching
-     * storage with less memory used.
-     * @return  either {@link Drawable} for xml and ColorDrawable <br>
-     *          or {@link InputStream} for Bitmap resource
-     */
-    private static Object loadDrawable(Context context, ShortcutIconResource r)
-            throws NameNotFoundException {
-        Resources resources = context.getPackageManager()
-                .getResourcesForApplication(r.packageName);
-        if (resources == null) {
-            return null;
-        }
-        resources.updateConfiguration(context.getResources().getConfiguration(),
-                context.getResources().getDisplayMetrics());
-        final int id = resources.getIdentifier(r.resourceName, null, null);
-        if (id == 0) {
-            Log.e(TAG, "Couldn't get resource " + r.resourceName + " in resources of "
-                    + r.packageName);
-            return null;
-        }
-        TypedValue value = new TypedValue();
-        resources.getValue(id, value, true);
-        if ((value.type == TypedValue.TYPE_STRING && value.string.toString().endsWith(".xml")) || (
-                value.type >= TypedValue.TYPE_FIRST_COLOR_INT
-                && value.type <= TypedValue.TYPE_LAST_COLOR_INT)) {
-            return resources.getDrawable(id);
-        }
-        return resources.openRawResource(id, value);
-    }
-
-    private static Bitmap createIconBitmap(Drawable drawable, BitmapWorkerOptions workerOptions) {
-        // Some drawables have an intrinsic width and height of -1. In that case
-        // size it to our output.
-        int width = drawable.getIntrinsicWidth();
-        if (width == -1) {
-            width = workerOptions.getWidth();
-        }
-        int height = drawable.getIntrinsicHeight();
-        if (height == -1) {
-            height = workerOptions.getHeight();
-        }
-        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(bitmap);
-        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
-        drawable.draw(canvas);
-        return bitmap;
-    }
-
-    public static Drawable getDrawable(Context context, ShortcutIconResource iconResource)
-            throws NameNotFoundException {
-        Resources resources =
-                context.getPackageManager().getResourcesForApplication(iconResource.packageName);
-        if (resources == null) {
-            return null;
-        }
-        resources.updateConfiguration(context.getResources().getConfiguration(),
-                context.getResources().getDisplayMetrics());
-        int id = resources.getIdentifier(iconResource.resourceName, null, null);
-        if (id == 0) {
-            throw new NameNotFoundException();
-        }
-
-        return resources.getDrawable(id);
-    }
-
-    @SuppressWarnings("deprecation")
-    private Bitmap getAccountImage(BitmapWorkerOptions options) {
-        String accountName = UriUtils.getAccountName(options.getResourceUri());
-        Context context = options.getContext();
-
-        if (accountName != null && context != null) {
-            Account thisAccount = null;
-            for (Account account : AccountManager.get(context).
-                    getAccountsByType(GOOGLE_ACCOUNT_TYPE)) {
-                if (account.name.equals(accountName)) {
-                    thisAccount = account;
-                    break;
-                }
-            }
-            if (thisAccount != null) {
-                String picUriString = AccountImageHelper.getAccountPictureUri(context, thisAccount);
-                if (picUriString != null) {
-                    BitmapWorkerOptions.Builder optionBuilder =
-                            new BitmapWorkerOptions.Builder(context)
-                            .width(options.getWidth())
-                                    .height(options.getHeight())
-                                    .cacheFlag(options.getCacheFlag())
-                                    .bitmapConfig(options.getBitmapConfig())
-                                    .resource(Uri.parse(picUriString));
-                    return BitmapDownloader.getInstance(context)
-                            .loadBitmapBlocking(optionBuilder.build());
-                }
-                return null;
-            }
-        }
-        return null;
-    }
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/ByteArrayPool.java b/car-apps-common/src/com/android/car/apps/common/ByteArrayPool.java
deleted file mode 100644
index 676b7dc..0000000
--- a/car-apps-common/src/com/android/car/apps/common/ByteArrayPool.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * @hide
- */
-public final class ByteArrayPool {
-
-    public static final int CHUNK16K = 16 * 1024;
-    public static final int DEFAULT_MAX_NUM = 8;
-
-    private final static ByteArrayPool sChunk16K = new ByteArrayPool(CHUNK16K, DEFAULT_MAX_NUM);
-
-    private final ArrayList<byte[]> mCachedBuf;
-    private final int mChunkSize;
-    private final int mMaxNum;
-
-    private ByteArrayPool(int chunkSize, int maxNum) {
-        mChunkSize = chunkSize;
-        mMaxNum = maxNum;
-        mCachedBuf = new ArrayList<byte[]>(mMaxNum);
-    }
-
-    /**
-     * get singleton of 16KB byte[] pool
-     */
-    public static ByteArrayPool get16KBPool() {
-        return sChunk16K;
-    }
-
-    public byte[] allocateChunk() {
-        synchronized (mCachedBuf) {
-            int size = mCachedBuf.size();
-            if (size > 0) {
-                return mCachedBuf.remove(size - 1);
-            }
-            return new byte[mChunkSize];
-        }
-    }
-
-    public void clear() {
-        synchronized (mCachedBuf) {
-            mCachedBuf.clear();
-        }
-    }
-
-    public void releaseChunk(byte[] buf) {
-        if (buf == null || buf.length != mChunkSize) {
-            return;
-        }
-        synchronized (mCachedBuf) {
-            if (mCachedBuf.size() < mMaxNum) {
-                mCachedBuf.add(buf);
-            }
-        }
-    }
-
-    public void releaseChunks(List<byte[]> bufs) {
-        synchronized (mCachedBuf) {
-            for (int i = 0, c = bufs.size(); i < c; i++) {
-                if (mCachedBuf.size() == mMaxNum) {
-                    break;
-                }
-                byte[] buf = bufs.get(i);
-                if (buf != null && buf.length == mChunkSize) {
-                    mCachedBuf.add(bufs.get(i));
-                }
-            }
-        }
-    }
-
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/CachedInputStream.java b/car-apps-common/src/com/android/car/apps/common/CachedInputStream.java
deleted file mode 100644
index f365424..0000000
--- a/car-apps-common/src/com/android/car/apps/common/CachedInputStream.java
+++ /dev/null
@@ -1,260 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A replacement of BufferedInputStream (no multiple thread): <p>
- * - use list of byte array (chunks) instead of keep growing a single byte array (more efficent)
- *   <br>
- * - support overriding the markLimit passed in mark() call (The value that BitmapFactory
- *   uses 1024 is too small for detecting bitmap bounds and reset()) <br>
- * @hide
- */
-public class CachedInputStream extends FilterInputStream {
-
-    private static final int CHUNK_SIZE = ByteArrayPool.CHUNK16K;
-
-    private ArrayList<byte[]> mBufs = new ArrayList<byte[]>();
-    private int mPos = 0;  // current read position inside the chunk buffers
-    private int mCount = 0; // total validate bytes in chunk buffers
-    private int mMarkPos = -1; // marked read position in chunk buffers
-    private int mOverrideMarkLimit; // to override readlimit of mark() call
-    private int mMarkLimit; // effective marklimit
-    private byte[] tmp = new byte[1]; // tmp buffer used in read()
-
-    public CachedInputStream(InputStream in) {
-        super(in);
-    }
-
-    @Override
-    public boolean markSupported() {
-        return true;
-    }
-
-    /**
-     * set the value that will override small readlimit passed in mark() call.
-     */
-    public void setOverrideMarkLimit(int overrideMarkLimit) {
-        mOverrideMarkLimit = overrideMarkLimit;
-    }
-
-    public int getOverrideMarkLimit() {
-        return mOverrideMarkLimit;
-    }
-
-    @Override
-    public void mark(int readlimit) {
-        readlimit = readlimit < mOverrideMarkLimit ? mOverrideMarkLimit : readlimit;
-        if (mMarkPos >= 0) {
-            // for replacing existing mark(), discard anything before mPos
-            // and move mMarkPos to mPos
-            int chunks = mPos / CHUNK_SIZE;
-            if (chunks > 0) {
-                // trim the header buffers
-                int removedBytes = chunks * CHUNK_SIZE;
-                List<byte[]> subList = mBufs.subList(0, chunks);
-                releaseChunks(subList);
-                subList.clear();
-                mPos = mPos - removedBytes;
-                mCount = mCount - removedBytes;
-            }
-        }
-        mMarkPos = mPos;
-        mMarkLimit = readlimit;
-    }
-
-    @Override
-    public void reset() throws IOException {
-        if (mMarkPos < 0) {
-            throw new IOException("mark has been invalidated");
-        }
-        mPos = mMarkPos;
-    }
-
-    @Override
-    public int read() throws IOException {
-        // TODO, not efficient, but the function is not called by BitmapFactory
-        int r = read(tmp, 0, 1);
-        if (r <= 0) {
-            return -1;
-        }
-        return tmp[0] & 0xFF;
-    }
-
-    @Override
-    public void close() throws IOException {
-        if (in!=null) {
-            in.close();
-            in = null;
-        }
-        releaseChunks(mBufs);
-    }
-
-    private static void releaseChunks(List<byte[]> bufs) {
-        ByteArrayPool.get16KBPool().releaseChunks(bufs);
-    }
-
-    private byte[] allocateChunk() {
-        return ByteArrayPool.get16KBPool().allocateChunk();
-    }
-
-    private boolean invalidate() {
-        if (mCount - mMarkPos > mMarkLimit) {
-            mMarkPos = -1;
-            mCount = 0;
-            mPos = 0;
-            releaseChunks(mBufs);
-            mBufs.clear();
-            return true;
-        }
-        return false;
-    }
-
-    @Override
-    public int read(byte[] buffer, int offset, int count) throws IOException {
-        if (in == null) {
-            throw streamClosed();
-        }
-        if (mMarkPos == -1) {
-            int reads = in.read(buffer, offset, count);
-            return reads;
-        }
-        if (count == 0) {
-            return 0;
-        }
-        int copied = copyMarkedBuffer(buffer, offset, count);
-        count -= copied;
-        offset += copied;
-        int totalReads = copied;
-        while (count > 0) {
-            if (mPos == mBufs.size() * CHUNK_SIZE) {
-                mBufs.add(allocateChunk());
-            }
-            int currentBuf = mPos / CHUNK_SIZE;
-            int indexInBuf = mPos - currentBuf * CHUNK_SIZE;
-            byte[] buf = mBufs.get(currentBuf);
-            int end = (currentBuf + 1) * CHUNK_SIZE;
-            int leftInBuffer = end - mPos;
-            int toRead = count > leftInBuffer ? leftInBuffer : count;
-            int reads = in.read(buf, indexInBuf, toRead);
-            if (reads > 0) {
-                System.arraycopy(buf, indexInBuf, buffer, offset, reads);
-                mPos += reads;
-                mCount += reads;
-                totalReads += reads;
-                offset += reads;
-                count -= reads;
-                if (invalidate()) {
-                    reads = in.read(buffer, offset, count);
-                    if (reads >0 ) {
-                        totalReads += reads;
-                    }
-                    break;
-                }
-            } else {
-                break;
-            }
-        }
-        if (totalReads == 0) {
-            return -1;
-        }
-        return totalReads;
-    }
-
-    private int copyMarkedBuffer(byte[] buffer, int offset, int read) {
-        int totalRead = 0;
-        while (read > 0 && mPos < mCount) {
-            int currentBuf = mPos / CHUNK_SIZE;
-            int indexInBuf = mPos - currentBuf * CHUNK_SIZE;
-            byte[] buf = mBufs.get(currentBuf);
-            int end = (currentBuf + 1) * CHUNK_SIZE;
-            if (end > mCount) {
-                end = mCount;
-            }
-            int leftInBuffer = end - mPos;
-            int toRead = read > leftInBuffer ? leftInBuffer : read;
-            System.arraycopy(buf, indexInBuf, buffer, offset, toRead);
-            offset += toRead;
-            read -= toRead;
-            totalRead += toRead;
-            mPos += toRead;
-        }
-        return totalRead;
-    }
-
-    @Override
-    public int available() throws IOException {
-        if (in == null) {
-            throw streamClosed();
-        }
-        return mCount - mPos + in.available();
-    }
-
-    @Override
-    public long skip(long byteCount) throws IOException {
-        if (in == null) {
-            throw streamClosed();
-        }
-        if (mMarkPos <0) {
-            return in.skip(byteCount);
-        }
-        long totalSkip = 0;
-        totalSkip = mCount - mPos;
-        if (totalSkip > byteCount) {
-            totalSkip = byteCount;
-        }
-        mPos += totalSkip;
-        byteCount -= totalSkip;
-        while (byteCount > 0) {
-            if (mPos == mBufs.size() * CHUNK_SIZE) {
-                mBufs.add(allocateChunk());
-            }
-            int currentBuf = mPos / CHUNK_SIZE;
-            int indexInBuf = mPos - currentBuf * CHUNK_SIZE;
-            byte[] buf = mBufs.get(currentBuf);
-            int end = (currentBuf + 1) * CHUNK_SIZE;
-            int leftInBuffer = end - mPos;
-            int toRead = (int) (byteCount > leftInBuffer ? leftInBuffer : byteCount);
-            int reads = in.read(buf, indexInBuf, toRead);
-            if (reads > 0) {
-                mPos += reads;
-                mCount += reads;
-                byteCount -= reads;
-                totalSkip += reads;
-                if (invalidate()) {
-                    if (byteCount > 0) {
-                        totalSkip += in.skip(byteCount);
-                    }
-                    break;
-                }
-            } else {
-                break;
-            }
-        }
-        return totalSkip;
-    }
-
-    private static IOException streamClosed() {
-        return new IOException("stream closed");
-    }
-
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/CarUxRestrictionsUtil.java b/car-apps-common/src/com/android/car/apps/common/CarUxRestrictionsUtil.java
index ed219ae..219bf59 100644
--- a/car-apps-common/src/com/android/car/apps/common/CarUxRestrictionsUtil.java
+++ b/car-apps-common/src/com/android/car/apps/common/CarUxRestrictionsUtil.java
@@ -38,6 +38,8 @@
  * This class must be a singleton because only one listener can be registered with
  * {@link CarUxRestrictionsManager} at a time, as documented in
  * {@link CarUxRestrictionsManager#registerListener}.
+ *
+ * @deprecated Use {@link com.android.car.ui.utils.CarUxRestrictionsUtil} instead
  */
 public class CarUxRestrictionsUtil {
     private static final String TAG = "CarUxRestrictionsUtil";
@@ -63,7 +65,7 @@
             }
         };
 
-        mCarApi = Car.createCar(context);
+        mCarApi = Car.createCar(context.getApplicationContext());
         mObservers = Collections.newSetFromMap(new WeakHashMap<>());
 
         try {
diff --git a/car-apps-common/src/com/android/car/apps/common/ClickThroughToolbar.java b/car-apps-common/src/com/android/car/apps/common/ClickThroughToolbar.java
index 9e3faec..7e5b79c 100644
--- a/car-apps-common/src/com/android/car/apps/common/ClickThroughToolbar.java
+++ b/car-apps-common/src/com/android/car/apps/common/ClickThroughToolbar.java
@@ -27,7 +27,10 @@
  *
  * <p>By default, the {@link Toolbar} eats all touches on it. This view will override
  * {@link #onTouchEvent(MotionEvent)} and return {@code false} if configured to allow pass through.
+ *
+ * @deprecated Use {@link com.android.car.ui.toolbar.Toolbar} instead
  */
+@Deprecated
 public class ClickThroughToolbar extends Toolbar {
     private boolean mAllowClickPassThrough;
 
diff --git a/car-apps-common/src/com/android/car/apps/common/CommonFlags.java b/car-apps-common/src/com/android/car/apps/common/CommonFlags.java
new file mode 100644
index 0000000..5fbc4a0
--- /dev/null
+++ b/car-apps-common/src/com/android/car/apps/common/CommonFlags.java
@@ -0,0 +1,74 @@
+/*
+ * 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.apps.common;
+
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.SystemProperties;
+
+/** Singleton class regrouping common library feature flags. */
+public class CommonFlags {
+
+    private static final String FLAG_IMPROPER_IMAGE_REFS_KEY =
+            "com.android.car.apps.common.FlagNonLocalImages";
+
+    @SuppressWarnings("StaticFieldLeak") // We store the application context, not an activity.
+    private static CommonFlags sInstance;
+
+    /** Returns the singleton. */
+    public static CommonFlags getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new CommonFlags(context);
+        }
+        return sInstance;
+    }
+
+    private final Context mApplicationContext;
+    private Boolean mFlagImproperImageRefs;
+
+    private CommonFlags(@NonNull Context context) {
+        mApplicationContext = context.getApplicationContext();
+    }
+
+    /**
+     * Returns whether improper image references should be flagged (typically tinting the images
+     * in red with {@link R.color.improper_image_refs_tint_color}. This special mode is intended for
+     * third party app developers so they can notice quickly that they are sending improper image
+     * references. Such references include :
+     * <li>remote image instead of a local content uri</li>
+     * <li>bitmap sent over the binder instead of a local content uri</li>
+     * <li>bitmap icon instead of a vector drawable</li>
+     * <p/>
+     *
+     * To activate, either overlay R.bool.flag_improper_image_references to true, or use adb:
+     * <code>
+     *     adb root
+     *     adb shell setprop com.android.car.apps.common.FlagNonLocalImages 1
+     *     adb shell am force-stop APP_PACKAGE # eg: APP_PACKAGE= com.android.car.media
+     * </code>
+     */
+    public boolean shouldFlagImproperImageRefs() {
+        if (mFlagImproperImageRefs == null) {
+            Resources res = mApplicationContext.getResources();
+            mFlagImproperImageRefs = res.getBoolean(R.bool.flag_improper_image_references)
+                    || "1".equals(SystemProperties.get(FLAG_IMPROPER_IMAGE_REFS_KEY, "0"));
+        }
+        return mFlagImproperImageRefs;
+    }
+}
diff --git a/car-apps-common/src/com/android/car/apps/common/DrawableDownloader.java b/car-apps-common/src/com/android/car/apps/common/DrawableDownloader.java
deleted file mode 100644
index a13fb18..0000000
--- a/car-apps-common/src/com/android/car/apps/common/DrawableDownloader.java
+++ /dev/null
@@ -1,406 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent.ShortcutIconResource;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.util.Log;
-import android.util.LruCache;
-import android.widget.ImageView;
-
-import java.lang.ref.SoftReference;
-import java.util.ArrayList;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * Downloader class which loads a resource URI into an image view or triggers a callback
- * <p>
- * This class adds a LRU cache over DrawableLoader.
- * <p>
- * Calling getBitmap() or loadBitmap() will return a RefcountBitmapDrawable with initial refcount =
- * 2 by the cache table and by caller.  You must call releaseRef() when you are done with the resource.
- * The most common way is using RefcountImageView, and releaseRef() for you.  Once both RefcountImageView
- * and LRUCache removes the refcount, the underlying bitmap will be used for decoding new bitmap.
- * <p>
- * If the URI does not point to a bitmap (e.g. point to a drawable xml, we won't cache it and we
- * directly return a regular Drawable).
- */
-public class DrawableDownloader {
-
-    private static final String TAG = "DrawableDownloader";
-
-    private static final boolean DEBUG = false;
-
-    private static final int CORE_POOL_SIZE = 5;
-
-    // thread pool for loading non android-resources such as http,  content
-    private static final Executor BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR =
-            Executors.newFixedThreadPool(CORE_POOL_SIZE);
-
-    private static final int CORE_RESOURCE_POOL_SIZE = 1;
-
-    // thread pool for loading android resources,  we use separate thread pool so
-    // that network loading will not block local android icons
-    private static final Executor BITMAP_RESOURCE_DOWNLOADER_THREAD_POOL_EXECUTOR =
-            Executors.newFixedThreadPool(CORE_RESOURCE_POOL_SIZE);
-
-    // 1/4 of max memory is used for bitmap mem cache
-    private static final int MEM_TO_CACHE = 4;
-
-    // hard limit for bitmap mem cache in MB
-    private static final int CACHE_HARD_LIMIT = 32;
-
-    /**
-     * bitmap cache item structure saved in LruCache
-     */
-    private static class BitmapItem {
-        int mOriginalWidth;
-        int mOriginalHeight;
-        ArrayList<BitmapDrawable> mBitmaps = new ArrayList<BitmapDrawable>(3);
-        int mByteCount;
-        public BitmapItem(int originalWidth, int originalHeight) {
-            mOriginalWidth = originalWidth;
-            mOriginalHeight = originalHeight;
-        }
-
-        // get bitmap from the list
-        BitmapDrawable findDrawable(BitmapWorkerOptions options) {
-            for (int i = 0, c = mBitmaps.size(); i < c; i++) {
-                BitmapDrawable d = mBitmaps.get(i);
-                // use drawable with original size
-                if (d.getIntrinsicWidth() == mOriginalWidth
-                        && d.getIntrinsicHeight() == mOriginalHeight) {
-                    return d;
-                }
-                // if specified width/height in options and is smaller than
-                // cached one, we can use this cached drawable
-                if (options.getHeight() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
-                    if (options.getHeight() <= d.getIntrinsicHeight()) {
-                        return d;
-                    }
-                } else if (options.getWidth() != BitmapWorkerOptions.MAX_IMAGE_DIMENSION_PX) {
-                    if (options.getWidth() <= d.getIntrinsicWidth()) {
-                        return d;
-                    }
-                }
-            }
-            return null;
-        }
-
-        @SuppressWarnings("unused")
-        BitmapDrawable findLargestDrawable(BitmapWorkerOptions options) {
-            return mBitmaps.size() == 0 ? null : mBitmaps.get(0);
-        }
-
-        void addDrawable(BitmapDrawable d) {
-            int i = 0, c = mBitmaps.size();
-            for (; i < c; i++) {
-                BitmapDrawable drawable = mBitmaps.get(i);
-                if (drawable.getIntrinsicHeight() < d.getIntrinsicHeight()) {
-                    break;
-                }
-            }
-            mBitmaps.add(i, d);
-            mByteCount += RecycleBitmapPool.getSize(d.getBitmap());
-        }
-
-        void clear() {
-            for (int i = 0, c = mBitmaps.size(); i < c; i++) {
-                BitmapDrawable d = mBitmaps.get(i);
-                if (d instanceof RefcountBitmapDrawable) {
-                    ((RefcountBitmapDrawable) d).getRefcountObject().releaseRef();
-                }
-            }
-            mBitmaps.clear();
-            mByteCount = 0;
-        }
-    }
-
-    public static abstract class BitmapCallback {
-        SoftReference<DrawableLoader> mTask;
-
-        public abstract void onBitmapRetrieved(Drawable bitmap);
-    }
-
-    private Context mContext;
-    private LruCache<String, BitmapItem> mMemoryCache;
-    private RecycleBitmapPool mRecycledBitmaps;
-
-    private static DrawableDownloader sBitmapDownloader;
-
-    private static final Object sBitmapDownloaderLock = new Object();
-
-    /**
-     * get the singleton BitmapDownloader for the application
-     */
-    public final static DrawableDownloader getInstance(Context context) {
-        if (sBitmapDownloader == null) {
-            synchronized(sBitmapDownloaderLock) {
-                if (sBitmapDownloader == null) {
-                    sBitmapDownloader = new DrawableDownloader(context);
-                }
-            }
-        }
-        return sBitmapDownloader;
-    }
-
-    private static String getBucketKey(String baseKey, Bitmap.Config bitmapConfig) {
-        return new StringBuilder(baseKey.length() + 16).append(baseKey)
-                         .append(":").append(bitmapConfig == null ? "" : bitmapConfig.ordinal())
-                         .toString();
-     }
-
-    public static Drawable getDrawable(Context context, ShortcutIconResource iconResource)
-            throws NameNotFoundException {
-        return DrawableLoader.getDrawable(context, iconResource);
-    }
-
-    private DrawableDownloader(Context context) {
-        mContext = context;
-        int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE))
-                .getMemoryClass();
-        memClass = memClass / MEM_TO_CACHE;
-        if (memClass > CACHE_HARD_LIMIT) {
-            memClass = CACHE_HARD_LIMIT;
-        }
-        int cacheSize = 1024 * 1024 * memClass;
-        mMemoryCache = new LruCache<String, BitmapItem>(cacheSize) {
-            @Override
-            protected int sizeOf(String key, BitmapItem bitmap) {
-                return bitmap.mByteCount;
-            }
-            @Override
-            protected void entryRemoved(
-                    boolean evicted, String key, BitmapItem oldValue, BitmapItem newValue) {
-                if (evicted) {
-                    oldValue.clear();
-                }
-            }
-        };
-        mRecycledBitmaps = new RecycleBitmapPool();
-    }
-
-    /**
-     * trim memory cache to 0~1 * maxSize
-     */
-    public void trimTo(float amount) {
-        if (amount == 0f) {
-            mMemoryCache.evictAll();
-        } else {
-            mMemoryCache.trimToSize((int) (amount * mMemoryCache.maxSize()));
-        }
-    }
-
-    /**
-     * load bitmap in current thread, will *block* current thread.
-     * @deprecated
-     */
-    @Deprecated
-    public final Drawable loadBitmapBlocking(BitmapWorkerOptions options) {
-        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
-        Drawable bitmap = null;
-        if (hasAccountImageUri) {
-            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
-        } else {
-            bitmap = getBitmapFromMemCache(options);
-        }
-
-        if (bitmap == null) {
-            DrawableLoader task = new DrawableLoader(null, mRecycledBitmaps) {
-                @Override
-                protected Drawable doInBackground(BitmapWorkerOptions... params) {
-                    final Drawable bitmap = super.doInBackground(params);
-                    if (bitmap != null && !hasAccountImageUri) {
-                        addBitmapToMemoryCache(params[0], bitmap, this);
-                    }
-                    return bitmap;
-                }
-            };
-            return task.doInBackground(options);
-        }
-        return bitmap;
-    }
-
-    /**
-     * Loads the bitmap into the image view.
-     */
-    public void loadBitmap(BitmapWorkerOptions options, final ImageView imageView) {
-        cancelDownload(imageView);
-        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
-        Drawable bitmap = null;
-        if (hasAccountImageUri) {
-            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
-        } else {
-            bitmap = getBitmapFromMemCache(options);
-        }
-
-        if (bitmap != null) {
-            imageView.setImageDrawable(bitmap);
-        } else {
-            DrawableLoader task = new DrawableLoader(imageView, mRecycledBitmaps) {
-                @Override
-                protected Drawable doInBackground(BitmapWorkerOptions... params) {
-                    Drawable bitmap = super.doInBackground(params);
-                    if (bitmap != null && !hasAccountImageUri) {
-                        addBitmapToMemoryCache(params[0], bitmap, this);
-                    }
-                    return bitmap;
-                }
-            };
-            imageView.setTag(R.id.imageDownloadTask, new SoftReference<DrawableLoader>(task));
-            scheduleTask(task, options);
-        }
-    }
-
-    /**
-     * Loads the bitmap.
-     * <p>
-     * This will be sent back to the callback object.
-     */
-    public void getBitmap(BitmapWorkerOptions options, final BitmapCallback callback) {
-        cancelDownload(callback);
-        final boolean hasAccountImageUri = UriUtils.isAccountImageUri(options.getResourceUri());
-        final Drawable bitmap = hasAccountImageUri ? null : getBitmapFromMemCache(options);
-        if (hasAccountImageUri) {
-            AccountImageChangeObserver.getInstance().registerChangeUriIfPresent(options);
-        }
-
-        if (bitmap != null) {
-            callback.onBitmapRetrieved(bitmap);
-            return;
-        }
-        DrawableLoader task = new DrawableLoader(null, mRecycledBitmaps) {
-            @Override
-            protected Drawable doInBackground(BitmapWorkerOptions... params) {
-                final Drawable bitmap = super.doInBackground(params);
-                if (bitmap != null && !hasAccountImageUri) {
-                    addBitmapToMemoryCache(params[0], bitmap, this);
-                }
-                return bitmap;
-            }
-
-            @Override
-            protected void onPostExecute(Drawable bitmap) {
-                callback.onBitmapRetrieved(bitmap);
-            }
-        };
-        callback.mTask = new SoftReference<DrawableLoader>(task);
-        scheduleTask(task, options);
-    }
-
-    private static void scheduleTask(DrawableLoader task, BitmapWorkerOptions options) {
-        if (options.isFromResource()) {
-            task.executeOnExecutor(BITMAP_RESOURCE_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
-        } else {
-            task.executeOnExecutor(BITMAP_DOWNLOADER_THREAD_POOL_EXECUTOR, options);
-        }
-    }
-
-    /**
-     * Cancel download<p>
-     * @param key {@link BitmapCallback} or {@link ImageView}
-     */
-    @SuppressWarnings("unchecked")
-    public boolean cancelDownload(Object key) {
-        DrawableLoader task = null;
-        if (key instanceof ImageView) {
-            ImageView imageView = (ImageView)key;
-            SoftReference<DrawableLoader> softReference =
-                    (SoftReference<DrawableLoader>) imageView.getTag(R.id.imageDownloadTask);
-            if (softReference != null) {
-                task = softReference.get();
-                softReference.clear();
-            }
-        } else if (key instanceof BitmapCallback) {
-            BitmapCallback callback = (BitmapCallback)key;
-            if (callback.mTask != null) {
-                task = callback.mTask.get();
-                callback.mTask = null;
-            }
-        }
-        if (task != null) {
-            return task.cancel(true);
-        }
-        return false;
-    }
-
-    private void addBitmapToMemoryCache(BitmapWorkerOptions key, Drawable bitmap,
-            DrawableLoader loader) {
-        if (!key.isMemCacheEnabled()) {
-            return;
-        }
-        if (!(bitmap instanceof BitmapDrawable)) {
-            return;
-        }
-        String bucketKey = getBucketKey(key.getCacheKey(), key.getBitmapConfig());
-        BitmapItem bitmapItem = mMemoryCache.get(bucketKey);
-        if (DEBUG) {
-            Log.d(TAG, "add cache "+bucketKey);
-        }
-        if (bitmapItem != null) {
-            // remove and re-add to update size
-            mMemoryCache.remove(bucketKey);
-        } else {
-            bitmapItem = new BitmapItem(loader.getOriginalWidth(), loader.getOriginalHeight());
-        }
-        if (bitmap instanceof RefcountBitmapDrawable) {
-            RefcountBitmapDrawable refcountDrawable = (RefcountBitmapDrawable) bitmap;
-            refcountDrawable.getRefcountObject().addRef();
-        }
-        bitmapItem.addDrawable((BitmapDrawable) bitmap);
-        mMemoryCache.put(bucketKey, bitmapItem);
-    }
-
-    private Drawable getBitmapFromMemCache(BitmapWorkerOptions key) {
-        String bucketKey =
-                getBucketKey(key.getCacheKey(), key.getBitmapConfig());
-        BitmapItem item = mMemoryCache.get(bucketKey);
-        if (item != null) {
-            return createRefCopy(item.findDrawable(key));
-        }
-        return null;
-    }
-
-    public BitmapDrawable getLargestBitmapFromMemCache(BitmapWorkerOptions key) {
-        String bucketKey =
-                getBucketKey(key.getCacheKey(), key.getBitmapConfig());
-        BitmapItem item = mMemoryCache.get(bucketKey);
-        if (item != null) {
-            return (BitmapDrawable) createRefCopy(item.findLargestDrawable(key));
-        }
-        return null;
-    }
-
-    private Drawable createRefCopy(Drawable d) {
-        if (d != null) {
-            if (d instanceof RefcountBitmapDrawable) {
-                RefcountBitmapDrawable refcountDrawable = (RefcountBitmapDrawable) d;
-                refcountDrawable.getRefcountObject().addRef();
-                d = new RefcountBitmapDrawable(mContext.getResources(),
-                        refcountDrawable);
-            }
-            return d;
-        }
-        return null;
-    }
-
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/DrawableLoader.java b/car-apps-common/src/com/android/car/apps/common/DrawableLoader.java
deleted file mode 100644
index a9379f0..0000000
--- a/car-apps-common/src/com/android/car/apps/common/DrawableLoader.java
+++ /dev/null
@@ -1,387 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.content.Context;
-import android.content.Intent.ShortcutIconResource;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.util.Log;
-import android.util.TypedValue;
-import android.widget.ImageView;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-import java.net.SocketTimeoutException;
-import java.net.URL;
-import java.net.URLConnection;
-
-/**
- * AsyncTask which loads a bitmap.
- * <p>
- * The source of this can be another package (via a resource), a URI (content provider), or
- * a file path.
- * @see BitmapWorkerOptions
- * @hide
- */
-class DrawableLoader extends AsyncTask<BitmapWorkerOptions, Void, Drawable> {
-
-    private static final String TAG = "DrawableLoader";
-    private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
-
-    private static final boolean DEBUG = false;
-
-    private static final int SOCKET_TIMEOUT = 10000;
-    private static final int READ_TIMEOUT = 10000;
-
-    private final WeakReference<ImageView> mImageView;
-    private int mOriginalWidth;
-    private int mOriginalHeight;
-    private final RecycleBitmapPool mRecycledBitmaps;
-
-    private final RefcountObject.RefcountListener mRefcountListener =
-            new RefcountObject.RefcountListener() {
-        @Override
-        @SuppressWarnings("rawtypes")
-        public void onRefcountZero(RefcountObject object) {
-            mRecycledBitmaps.addRecycledBitmap((Bitmap) object.getObject());
-        }
-    };
-
-
-    DrawableLoader(ImageView imageView, RecycleBitmapPool recycledBitmapPool) {
-        mImageView = new WeakReference<ImageView>(imageView);
-        mRecycledBitmaps = recycledBitmapPool;
-    }
-
-    public int getOriginalWidth() {
-        return mOriginalWidth;
-    }
-
-    public int getOriginalHeight() {
-        return mOriginalHeight;
-    }
-
-    @Override
-    protected Drawable doInBackground(BitmapWorkerOptions... params) {
-
-        return retrieveDrawable(params[0]);
-    }
-
-    protected Drawable retrieveDrawable(BitmapWorkerOptions workerOptions) {
-        try {
-            if (workerOptions.getIconResource() != null) {
-                return getBitmapFromResource(workerOptions.getIconResource(), workerOptions);
-            } else if (workerOptions.getResourceUri() != null) {
-                if (UriUtils.isAndroidResourceUri(workerOptions.getResourceUri())
-                        || UriUtils.isShortcutIconResourceUri(workerOptions.getResourceUri())) {
-                    // Make an icon resource from this.
-                    return getBitmapFromResource(
-                            UriUtils.getIconResource(workerOptions.getResourceUri()),
-                            workerOptions);
-                } else if (UriUtils.isWebUri(workerOptions.getResourceUri())) {
-                    return getBitmapFromHttp(workerOptions);
-                } else if (UriUtils.isContentUri(workerOptions.getResourceUri())) {
-                    return getBitmapFromContent(workerOptions);
-                } else if (UriUtils.isAccountImageUri(workerOptions.getResourceUri())) {
-                    return getAccountImage(workerOptions);
-                } else {
-                    Log.e(TAG, "Error loading bitmap - unknown resource URI! "
-                            + workerOptions.getResourceUri());
-                }
-            } else {
-                Log.e(TAG, "Error loading bitmap - no source!");
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Error loading url " + workerOptions.getResourceUri(), e);
-            return null;
-        } catch (RuntimeException e) {
-            Log.e(TAG, "Critical Error loading url " + workerOptions.getResourceUri(), e);
-            return null;
-        }
-
-        return null;
-    }
-
-    @Override
-    protected void onPostExecute(Drawable bitmap) {
-        if (mImageView != null) {
-            final ImageView imageView = mImageView.get();
-            if (imageView != null) {
-                imageView.setImageDrawable(bitmap);
-            }
-        }
-    }
-
-    @Override
-    protected void onCancelled(Drawable result) {
-        if (result instanceof RefcountBitmapDrawable) {
-            // Remove the extra refcount created by us,  DrawableDownloader LruCache
-            // still holds one to the bitmap
-            RefcountBitmapDrawable d = (RefcountBitmapDrawable) result;
-            d.getRefcountObject().releaseRef();
-        }
-    }
-
-    private Drawable getBitmapFromResource(ShortcutIconResource iconResource,
-            BitmapWorkerOptions outputOptions) throws IOException {
-        if (DEBUG) {
-            Log.d(TAG, "Loading " + iconResource.toString());
-        }
-        try {
-            Object drawable = loadDrawable(outputOptions.getContext(), iconResource);
-            if (drawable instanceof InputStream) {
-                // Most of these are bitmaps, so resize properly.
-                return decodeBitmap((InputStream)drawable, outputOptions);
-            } else if (drawable instanceof Drawable){
-                Drawable d = (Drawable) drawable;
-                mOriginalWidth = d.getIntrinsicWidth();
-                mOriginalHeight = d.getIntrinsicHeight();
-                return d;
-            } else {
-                Log.w(TAG, "getBitmapFromResource failed, unrecognized resource: " + drawable);
-                return null;
-            }
-        } catch (NameNotFoundException e) {
-            Log.w(TAG, "Could not load package: " + iconResource.packageName + "! NameNotFound");
-            return null;
-        } catch (NotFoundException e) {
-            Log.w(TAG, "Could not load resource: " + iconResource.resourceName + "! NotFound");
-            return null;
-        }
-    }
-
-    private Drawable decodeBitmap(InputStream in, BitmapWorkerOptions options)
-            throws IOException {
-        CachedInputStream bufferedStream = null;
-        BitmapFactory.Options bitmapOptions = null;
-        try {
-            bufferedStream = new CachedInputStream(in);
-            // Let the bufferedStream be able to mark unlimited bytes up to full stream length.
-            // The value that BitmapFactory uses (1024) is too small for detecting bounds
-            bufferedStream.setOverrideMarkLimit(Integer.MAX_VALUE);
-            bitmapOptions = new BitmapFactory.Options();
-            bitmapOptions.inJustDecodeBounds = true;
-            if (options.getBitmapConfig() != null) {
-                bitmapOptions.inPreferredConfig = options.getBitmapConfig();
-            }
-            bitmapOptions.inTempStorage = ByteArrayPool.get16KBPool().allocateChunk();
-            bufferedStream.mark(Integer.MAX_VALUE);
-            BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
-
-            mOriginalWidth = bitmapOptions.outWidth;
-            mOriginalHeight = bitmapOptions.outHeight;
-            int heightScale = 1;
-            int height = options.getHeight();
-            if (height > 0) {
-                heightScale = bitmapOptions.outHeight / height;
-            }
-
-            int widthScale = 1;
-            int width = options.getWidth();
-            if (width > 0) {
-                widthScale = bitmapOptions.outWidth / width;
-            }
-
-            int scale = heightScale > widthScale ? heightScale : widthScale;
-            if (scale <= 1) {
-                scale = 1;
-            } else {
-                int shift = 0;
-                do {
-                    scale >>= 1;
-                    shift++;
-                } while (scale != 0);
-                scale = 1 << (shift - 1);
-            }
-
-            if (DEBUG) {
-                Log.d("BitmapWorkerTask", "Source bitmap: (" + bitmapOptions.outWidth + "x"
-                        + bitmapOptions.outHeight + ").  Max size: (" + options.getWidth() + "x"
-                        + options.getHeight() + ").  Chosen scale: " + scale + " -> " + scale);
-            }
-
-            // Reset buffer to original position and disable the overrideMarkLimit
-            bufferedStream.reset();
-            bufferedStream.setOverrideMarkLimit(0);
-            Bitmap bitmap = null;
-            try {
-                bitmapOptions.inJustDecodeBounds = false;
-                bitmapOptions.inSampleSize = scale;
-                bitmapOptions.inMutable = true;
-                bitmapOptions.inBitmap = mRecycledBitmaps.getRecycledBitmap(
-                        mOriginalWidth / scale, mOriginalHeight / scale);
-                bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
-            } catch (RuntimeException ex) {
-                Log.e(TAG, "RuntimeException" + ex + ", trying decodeStream again");
-                bufferedStream.reset();
-                bufferedStream.setOverrideMarkLimit(0);
-                bitmapOptions.inBitmap = null;
-                bitmap = BitmapFactory.decodeStream(bufferedStream, null, bitmapOptions);
-            }
-            if (bitmap == null) {
-                Log.d(TAG, "bitmap was null");
-                return null;
-            }
-            RefcountObject<Bitmap> object = new RefcountObject<Bitmap>(bitmap);
-            object.addRef();
-            object.setRefcountListener(mRefcountListener);
-            RefcountBitmapDrawable d = new RefcountBitmapDrawable(
-                    options.getContext().getResources(), object);
-            return d;
-        } finally {
-            Log.w(TAG, "couldn't load bitmap, releasing resources");
-            if (bitmapOptions != null) {
-                ByteArrayPool.get16KBPool().releaseChunk(bitmapOptions.inTempStorage);
-            }
-            if (bufferedStream != null) {
-                bufferedStream.close();
-            }
-        }
-    }
-
-    private Drawable getBitmapFromHttp(BitmapWorkerOptions options) throws IOException {
-        URL url = new URL(options.getResourceUri().toString());
-        if (DEBUG) {
-            Log.d(TAG, "Loading " + url);
-        }
-        try {
-            // TODO use volley for better disk cache
-            URLConnection connection = url.openConnection();
-            connection.setConnectTimeout(SOCKET_TIMEOUT);
-            connection.setReadTimeout(READ_TIMEOUT);
-            InputStream in = connection.getInputStream();
-            return decodeBitmap(in, options);
-        } catch (SocketTimeoutException e) {
-            Log.e(TAG, "loading " + url + " timed out");
-        }
-        return null;
-    }
-
-    private Drawable getBitmapFromContent(BitmapWorkerOptions options)
-            throws IOException {
-        Uri resourceUri = options.getResourceUri();
-        if (resourceUri != null) {
-            try {
-                InputStream bitmapStream =
-                        options.getContext().getContentResolver().openInputStream(resourceUri);
-
-                if (bitmapStream != null) {
-                    return decodeBitmap(bitmapStream, options);
-                } else {
-                    Log.w(TAG, "Content provider returned a null InputStream when trying to " +
-                            "open resource.");
-                    return null;
-                }
-            } catch (FileNotFoundException e) {
-                Log.e(TAG, "FileNotFoundException during openInputStream for uri: "
-                        + resourceUri.toString());
-                return null;
-            }
-        } else {
-            Log.w(TAG, "Get null resourceUri from BitmapWorkerOptions.");
-            return null;
-        }
-    }
-
-    /**
-     * load drawable for non-bitmap resource or InputStream for bitmap resource without
-     * caching Bitmap in Resources.  So that caller can maintain a different caching
-     * storage with less memory used.
-     * @return  either {@link Drawable} for xml and ColorDrawable <br>
-     *          or {@link InputStream} for Bitmap resource
-     */
-    private static Object loadDrawable(Context context, ShortcutIconResource r)
-            throws NameNotFoundException {
-        Resources resources = context.getPackageManager()
-                .getResourcesForApplication(r.packageName);
-        if (resources == null) {
-            return null;
-        }
-        resources.updateConfiguration(context.getResources().getConfiguration(),
-                context.getResources().getDisplayMetrics());
-        final int id = resources.getIdentifier(r.resourceName, null, null);
-        if (id == 0) {
-            Log.e(TAG, "Couldn't get resource " + r.resourceName + " in resources of "
-                    + r.packageName);
-            return null;
-        }
-        TypedValue value = new TypedValue();
-        resources.getValue(id, value, true);
-        if ((value.type == TypedValue.TYPE_STRING && value.string.toString().endsWith(".xml")) || (
-                value.type >= TypedValue.TYPE_FIRST_COLOR_INT
-                && value.type <= TypedValue.TYPE_LAST_COLOR_INT)) {
-            return resources.getDrawable(id);
-        }
-        return resources.openRawResource(id, value);
-    }
-
-    public static Drawable getDrawable(Context context, ShortcutIconResource iconResource)
-            throws NameNotFoundException {
-        Resources resources =
-                context.getPackageManager().getResourcesForApplication(iconResource.packageName);
-        resources.updateConfiguration(context.getResources().getConfiguration(),
-                context.getResources().getDisplayMetrics());
-        int id = resources.getIdentifier(iconResource.resourceName, null, null);
-        if (id == 0) {
-            throw new NameNotFoundException();
-        }
-        return resources.getDrawable(id);
-    }
-
-    @SuppressWarnings("deprecation")
-    private Drawable getAccountImage(BitmapWorkerOptions options) {
-        String accountName = UriUtils.getAccountName(options.getResourceUri());
-        Context context = options.getContext();
-
-        if (accountName != null && context != null) {
-            Account thisAccount = null;
-            for (Account account : AccountManager.get(context).
-                    getAccountsByType(GOOGLE_ACCOUNT_TYPE)) {
-                if (account.name.equals(accountName)) {
-                    thisAccount = account;
-                    break;
-                }
-            }
-            if (thisAccount != null) {
-                String picUriString = AccountImageHelper.getAccountPictureUri(context, thisAccount);
-                if (picUriString != null) {
-                    BitmapWorkerOptions.Builder optionBuilder =
-                            new BitmapWorkerOptions.Builder(context)
-                            .width(options.getWidth())
-                                    .height(options.getHeight())
-                                    .cacheFlag(options.getCacheFlag())
-                                    .bitmapConfig(options.getBitmapConfig())
-                                    .resource(Uri.parse(picUriString));
-                    return DrawableDownloader.getInstance(context)
-                            .loadBitmapBlocking(optionBuilder.build());
-                }
-                return null;
-            }
-        }
-        return null;
-    }
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/ImageUtils.java b/car-apps-common/src/com/android/car/apps/common/ImageUtils.java
index 0fae806..e110e76 100644
--- a/car-apps-common/src/com/android/car/apps/common/ImageUtils.java
+++ b/car-apps-common/src/com/android/car/apps/common/ImageUtils.java
@@ -23,6 +23,7 @@
 import android.renderscript.Element;
 import android.renderscript.RenderScript;
 import android.renderscript.ScriptIntrinsicBlur;
+import android.util.Size;
 
 /**
  * Utility methods to manipulate images.
@@ -37,7 +38,7 @@
      * blurring radius.
      */
     @NonNull
-    public static Bitmap blur(Context context, @NonNull Bitmap image, int bitmapTargetSize,
+    public static Bitmap blur(Context context, @NonNull Bitmap image, Size bitmapTargetSize,
             float bitmapBlurPercent) {
         image = maybeResize(image, bitmapTargetSize);
         float blurRadius = bitmapBlurPercent * getBitmapDimension(image);
@@ -63,10 +64,11 @@
         return outputBitmap;
     }
 
-    private static Bitmap maybeResize(@NonNull Bitmap image, int bitmapTargetSize) {
-        int imageDim = getBitmapDimension(image);
-        if (imageDim > bitmapTargetSize) {
-            float scale = bitmapTargetSize / (float) imageDim;
+    private static Bitmap maybeResize(@NonNull Bitmap image, Size bitmapTargetSize) {
+        if (image.getWidth() > bitmapTargetSize.getWidth()
+                || image.getHeight() > bitmapTargetSize.getHeight()) {
+            int imageDim = getBitmapDimension(image);
+            float scale = getAverage(bitmapTargetSize) / (float) imageDim;
             int width = Math.round(scale * image.getWidth());
             int height = Math.round(scale * image.getHeight());
             return Bitmap.createScaledBitmap(image, width, height, false);
@@ -75,6 +77,10 @@
         }
     }
 
+    private static int getAverage(@NonNull Size size) {
+        return (size.getWidth() + size.getHeight()) / 2;
+    }
+
     private static int getBitmapDimension(@NonNull Bitmap image) {
         return (image.getWidth() + image.getHeight()) / 2;
     }
diff --git a/car-apps-common/src/com/android/car/apps/common/LetterTileDrawable.java b/car-apps-common/src/com/android/car/apps/common/LetterTileDrawable.java
index 1f960ea..3a698b0 100644
--- a/car-apps-common/src/com/android/car/apps/common/LetterTileDrawable.java
+++ b/car-apps-common/src/com/android/car/apps/common/LetterTileDrawable.java
@@ -15,7 +15,6 @@
  */
 package com.android.car.apps.common;
 
-import android.annotation.Nullable;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
@@ -29,6 +28,8 @@
 import android.graphics.drawable.Drawable;
 import android.text.TextUtils;
 
+import androidx.annotation.Nullable;
+
 /**
  * A drawable that encapsulates all the functionality needed to display a letter tile to
  * represent a contact image.
@@ -46,7 +47,6 @@
     /** Reusable components to avoid new allocations */
     private static final Paint sPaint = new Paint();
     private static final Rect sRect = new Rect();
-    private static final char[] sFirstChar = new char[1];
 
     /** Contact type constants */
     public static final int TYPE_PERSON = 1;
@@ -56,30 +56,43 @@
 
     private final Paint mPaint;
 
-    @Nullable private String mDisplayName;
+    private String mLetters;
     private int mColor;
     private int mContactType = TYPE_DEFAULT;
     private float mScale = 1.0f;
     private float mOffset = 0.0f;
     private boolean mIsCircle = false;
 
-    // TODO(rogerxue): the use pattern for this class is always:
-    // create LTD, setContactDetails(), setIsCircular(true). merge them into ctor.
+    /**
+     * A custom Drawable that draws letters on a colored background.
+     */
+    // The use pattern for this constructor is:
+    // create LTD, setContactDetails(), and setIsCircular(true) if needed.
     public LetterTileDrawable(final Resources res) {
+        this(res, null, null);
+    }
+
+    /**
+     * A custom Drawable that draws letters on a colored background.
+     */
+    // This constructor allows passing the letters and identifier directly. There is no need to
+    // call setContactDetails() again. setIsCircular(true) needs to be called separately if needed.
+    public LetterTileDrawable(final Resources res, @Nullable String letters,
+            @Nullable String identifier) {
         mPaint = new Paint();
         mPaint.setFilterBitmap(true);
         mPaint.setDither(true);
         setScale(0.7f);
 
         if (sColors == null) {
-            sDefaultColor = res.getColor(R.color.letter_tile_default_color);
+            sDefaultColor = res.getColor(R.color.letter_tile_default_color, null /* theme */);
             TypedArray ta = res.obtainTypedArray(R.array.letter_tile_colors);
             if (ta.length() == 0) {
                 // TODO(dnotario). Looks like robolectric shadow doesn't currently support
                 // obtainTypedArray and always returns length 0 array, which will make some code
                 // below that does a division by length of sColors choke. Workaround by creating
                 // an array of length 1. A more proper fix tracked by b/26518438.
-                sColors = new int[] { sDefaultColor };
+                sColors = new int[]{sDefaultColor};
 
             } else {
                 sColors = new int[ta.length()];
@@ -89,16 +102,20 @@
                 ta.recycle();
             }
 
-            sTileFontColor = res.getColor(R.color.letter_tile_font_color);
+            sTileFontColor = res.getColor(R.color.letter_tile_font_color, null /* theme */);
             sLetterToTileRatio = res.getFraction(R.fraction.letter_to_tile_ratio, 1, 1);
             // TODO: get images for business and voicemail
             sDefaultPersonAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
             sDefaultBusinessAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
             sDefaultVoicemailAvatar = res.getDrawable(R.drawable.ic_person, null /* theme */);
-            sPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
+            sPaint.setTypeface(
+                    Typeface.create(res.getString(R.string.config_letter_tile_font_family),
+                            res.getInteger(R.integer.config_letter_tile_text_style)));
             sPaint.setTextAlign(Align.CENTER);
             sPaint.setAntiAlias(true);
         }
+
+        setContactDetails(letters, identifier);
     }
 
     @Override
@@ -148,20 +165,16 @@
             canvas.drawRect(bounds, sPaint);
         }
 
-        // Draw letter/digit only if the first character is an english letter
-        if (!TextUtils.isEmpty(mDisplayName) && isEnglishLetter(mDisplayName.charAt(0))) {
-            // Draw letter or digit.
-            sFirstChar[0] = Character.toUpperCase(mDisplayName.charAt(0));
-
+        if (!TextUtils.isEmpty(mLetters)) {
             // Scale text by canvas bounds and user selected scaling factor
             sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
             //sPaint.setTextSize(sTileLetterFontSize);
-            sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
+            sPaint.getTextBounds(mLetters, 0, mLetters.length(), sRect);
             sPaint.setColor(sTileFontColor);
 
             // Draw the letter in the canvas, vertically shifted up or down by the user-defined
             // offset
-            canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
+            canvas.drawText(mLetters, 0, mLetters.length(), bounds.centerX(),
                     bounds.centerY() + mOffset * bounds.height() + sRect.height() / 2,
                     sPaint);
         } else {
@@ -202,10 +215,6 @@
         }
     }
 
-    private static boolean isEnglishLetter(final char c) {
-        return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
-    }
-
     @Override
     public void setAlpha(final int alpha) {
         mPaint.setAlpha(alpha);
@@ -225,7 +234,7 @@
      * Scale the drawn letter tile to a ratio of its default size
      *
      * @param scale The ratio the letter tile should be scaled to as a percentage of its default
-     * size, from a scale of 0 to 2.0f. The default is 1.0f.
+     *              size, from a scale of 0 to 2.0f. The default is 1.0f.
      */
     public void setScale(float scale) {
         mScale = scale;
@@ -235,20 +244,26 @@
      * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
      *
      * @param offset The provided offset must be within the range of -0.5f to 0.5f.
-     * If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of the canvas
-     * it is being drawn on, which means it will be drawn with the center of the letter starting
-     * at the top edge of the canvas.
-     * If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of the canvas
-     * it is being drawn on, which means it will be drawn with the center of the letter starting
-     * at the bottom edge of the canvas.
-     * The default is 0.0f.
+     *               If set to -0.5f, the letter will be shifted upwards by 0.5 times the height of
+     *               the canvas it is being drawn on, which means it will be drawn with the center
+     *               of the letter starting at the top edge of the canvas.
+     *               If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of
+     *               the canvas it is being drawn on, which means it will be drawn with the center
+     *               of the letter starting at the bottom edge of the canvas.
+     *               The default is 0.0f.
      */
     public void setOffset(float offset) {
         mOffset = offset;
     }
 
-    public void setContactDetails(@Nullable String displayName, String identifier) {
-        mDisplayName = displayName;
+    /**
+     * Sets the details.
+     *
+     * @param letters    The letters need to be drawn
+     * @param identifier decides the color for the drawable.
+     */
+    public void setContactDetails(@Nullable String letters, @Nullable String identifier) {
+        mLetters = letters;
         mColor = pickColor(identifier);
     }
 
@@ -262,6 +277,7 @@
 
     /**
      * Convert the drawable to a bitmap.
+     *
      * @param size The target size of the bitmap.
      * @return A bitmap representation of the drawable.
      */
diff --git a/car-apps-common/src/com/android/car/apps/common/RecycleBitmapPool.java b/car-apps-common/src/com/android/car/apps/common/RecycleBitmapPool.java
deleted file mode 100644
index 480ae00..0000000
--- a/car-apps-common/src/com/android/car/apps/common/RecycleBitmapPool.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import android.graphics.Bitmap;
-import android.util.Log;
-import android.util.SparseArray;
-
-import java.lang.ref.SoftReference;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-
-
-/**
- * This class saves recycle bitmap as SoftReference,  which is too vulnerable to
- * be garbage collected due to other part of application is re-allocating lots of
- * memory,  we will lose all SoftReference in a GC run.  We should maintain
- * certain amount of recycled bitmaps in memory, we may also need remove bitmap from LRUCache
- * if we are not able to get a recycled bitmap here.
- *
- * @hide
- */
-public class RecycleBitmapPool {
-
-    private static final String TAG = "RecycleBitmapPool";
-    private static final boolean DEBUG = false;
-    // allow reuse bitmap with larger bytes, set to 0 to disable different byte size
-    private static final int LARGER_BITMAP_ALLOWED_REUSE = 0;
-    private static final boolean LARGER_BITMAP_ALLOWED = LARGER_BITMAP_ALLOWED_REUSE > 0;
-
-    private static Method sGetAllocationByteCount;
-
-    static {
-        try {
-            // KLP or later
-            sGetAllocationByteCount = Bitmap.class.getMethod("getAllocationByteCount");
-        } catch (NoSuchMethodException e) {
-        }
-    }
-
-    private final SparseArray<ArrayList<SoftReference<Bitmap>>> mRecycled8888 =
-            new SparseArray<ArrayList<SoftReference<Bitmap>>>();
-
-    public RecycleBitmapPool() {
-    }
-
-    public static int getSize(Bitmap bitmap) {
-        if (sGetAllocationByteCount != null) {
-            try {
-                return (Integer) sGetAllocationByteCount.invoke(bitmap);
-            } catch (IllegalArgumentException e) {
-                Log.e(TAG, "getAllocationByteCount() failed", e);
-            } catch (IllegalAccessException e) {
-                Log.e(TAG, "getAllocationByteCount() failed", e);
-            } catch (InvocationTargetException e) {
-                Log.e(TAG, "getAllocationByteCount() failed", e);
-            }
-            sGetAllocationByteCount = null;
-        }
-        return bitmap.getByteCount();
-    }
-
-    private static int getSize(int width, int height) {
-        if (width >= 2048 || height >= 2048) {
-            return 0;
-        }
-        return width * height * 4;
-    }
-
-    public void addRecycledBitmap(Bitmap bitmap) {
-        if (bitmap.isRecycled()) {
-            return;
-        }
-        Bitmap.Config config = bitmap.getConfig();
-        if (config != Bitmap.Config.ARGB_8888) {
-            return;
-        }
-        int key = getSize(bitmap);
-        if (key == 0) {
-            return;
-        }
-        synchronized (mRecycled8888) {
-            ArrayList<SoftReference<Bitmap>> list = mRecycled8888.get(key);
-            if (list == null) {
-                list = new ArrayList<SoftReference<Bitmap>>();
-                mRecycled8888.put(key, list);
-            }
-            list.add(new SoftReference<Bitmap>(bitmap));
-            if (DEBUG) {
-                Log.d(TAG, list.size() + " add bitmap " + bitmap.getWidth() + " "
-                        + bitmap.getHeight());
-            }
-        }
-    }
-
-    public Bitmap getRecycledBitmap(int width, int height) {
-        int key = getSize(width, height);
-        if (key == 0) {
-            return null;
-        }
-        synchronized (mRecycled8888) {
-            // for the new version with getAllocationByteCount(), we allow larger size
-            // to be reused for the bitmap,  otherwise we just looks for same size
-            Bitmap bitmap = getRecycledBitmap(mRecycled8888.get(key));
-            if (sGetAllocationByteCount == null || bitmap != null) {
-                return bitmap;
-            }
-            if (LARGER_BITMAP_ALLOWED) {
-                for (int i = 0, c = mRecycled8888.size(); i < c; i++) {
-                    int k = mRecycled8888.keyAt(i);
-                    if (k > key && k <= key * LARGER_BITMAP_ALLOWED_REUSE) {
-                        bitmap = getRecycledBitmap(mRecycled8888.valueAt(i));
-                        if (bitmap != null) {
-                            return bitmap;
-                        }
-                    }
-                }
-            }
-        }
-        if (DEBUG) {
-            Log.d(TAG, "not avaialbe for " + width + "," + height);
-        }
-        return null;
-    }
-
-    private static Bitmap getRecycledBitmap(ArrayList<SoftReference<Bitmap>> list) {
-        if (list != null && !list.isEmpty()) {
-            while (!list.isEmpty()) {
-                SoftReference<Bitmap> ref = list.remove(list.size() - 1);
-                Bitmap bitmap = ref.get();
-                if (bitmap != null && !bitmap.isRecycled()) {
-                    if (DEBUG) {
-                        Log.d(TAG, "reuse " + bitmap.getWidth() + " " + bitmap.getHeight());
-                    }
-                    return bitmap;
-                } else {
-                    if (DEBUG) {
-                        Log.d(TAG, " we lost SoftReference to bitmap");
-                    }
-                }
-            }
-        }
-        return null;
-    }
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/RefcountBitmapDrawable.java b/car-apps-common/src/com/android/car/apps/common/RefcountBitmapDrawable.java
deleted file mode 100644
index 01a4bb1..0000000
--- a/car-apps-common/src/com/android/car/apps/common/RefcountBitmapDrawable.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-
-/**
- * RefcountBitmapDrawable class
- * @hide
- */
-public class RefcountBitmapDrawable extends BitmapDrawable {
-
-    private RefcountObject<Bitmap> mRefcountObject;
-
-    /**
-     *  create initial drawable,  this will not increase the refcount
-     */
-    public RefcountBitmapDrawable(Resources res, RefcountObject<Bitmap> bitmap) {
-        super(res, bitmap.getObject());
-        mRefcountObject = bitmap;
-    }
-
-    /**
-     *  create the drawable from existing drawable, will not increase refcount
-     */
-    public RefcountBitmapDrawable(Resources res, RefcountBitmapDrawable drawable) {
-        this(res, drawable.getRefcountObject());
-    }
-
-    public RefcountObject<Bitmap> getRefcountObject() {
-        return mRefcountObject;
-    }
-}
diff --git a/car-apps-common/src/com/android/car/apps/common/UriUtils.java b/car-apps-common/src/com/android/car/apps/common/UriUtils.java
index f17034b..199635b 100644
--- a/car-apps-common/src/com/android/car/apps/common/UriUtils.java
+++ b/car-apps-common/src/com/android/car/apps/common/UriUtils.java
@@ -15,10 +15,14 @@
  */
 package com.android.car.apps.common;
 
+import static android.content.pm.PackageManager.MATCH_ALL;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent.ShortcutIconResource;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ProviderInfo;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
@@ -51,6 +55,11 @@
      */
     private UriUtils() {}
 
+    /** Returns true if the uri is null or empty. */
+    public static boolean isEmpty(@Nullable Uri uri) {
+        return (uri == null || TextUtils.isEmpty(uri.toString()));
+    }
+
     /**
      * Gets resource uri representation for a resource of a package
      */
@@ -172,6 +181,18 @@
     }
 
     /**
+     * Finds the packageName of the application to which the content authority of the given uri
+     * belongs to.
+     */
+    @Nullable
+    public static String getPackageName(Context context, Uri uri) {
+        PackageManager pm = context.getPackageManager();
+        ProviderInfo info = pm.resolveContentProvider(uri.getAuthority(), MATCH_ALL);
+        // Info can be null when the app doesn't define a provider.
+        return (info != null) ? info.packageName : uri.getAuthority();
+    }
+
+    /**
      * Returns {@code true} if the URI refers to a content URI which can be opened via
      * {@link ContentResolver#openInputStream(Uri)}.
      */
@@ -190,21 +211,24 @@
     /**
      * Creates a shortcut icon resource object from an Android resource URI.
      */
-    public static ShortcutIconResource getIconResource(Uri uri) {
+    public static ShortcutIconResource getIconResource(Context context, Uri uri) {
         if(isAndroidResourceUri(uri)) {
             ShortcutIconResource iconResource = new ShortcutIconResource();
-            iconResource.packageName = uri.getAuthority();
-            // Trim off the scheme + 3 extra for "://", then replace the first "/" with a ":"
-            iconResource.resourceName = uri.toString().substring(
-                    ContentResolver.SCHEME_ANDROID_RESOURCE.length() + SCHEME_DELIMITER.length())
-                    .replaceFirst(URI_PATH_DELIMITER, URI_PACKAGE_DELIMITER);
+            iconResource.packageName = getPackageName(context, uri);
+            // Trim off the scheme + 3 extra for "://" + authority, then replace the first "/"
+            // with a ":" and add to packageName.
+            int resStart = ContentResolver.SCHEME_ANDROID_RESOURCE.length()
+                    + SCHEME_DELIMITER.length() + uri.getAuthority().length();
+            iconResource.resourceName = iconResource.packageName
+                    + uri.toString().substring(resStart)
+                            .replaceFirst(URI_PATH_DELIMITER, URI_PACKAGE_DELIMITER);
             return iconResource;
         } else if(isShortcutIconResourceUri(uri)) {
             ShortcutIconResource iconResource = new ShortcutIconResource();
-            iconResource.packageName = uri.getAuthority();
+            iconResource.packageName = getPackageName(context, uri);
             iconResource.resourceName = uri.toString().substring(
                     SCHEME_SHORTCUT_ICON_RESOURCE.length() + SCHEME_DELIMITER.length()
-                    + iconResource.packageName.length() + URI_PATH_DELIMITER.length())
+                    + uri.getAuthority().length() + URI_PATH_DELIMITER.length())
                     .replaceFirst(URI_PATH_DELIMITER, URI_PACKAGE_DELIMITER);
             return iconResource;
         } else {
diff --git a/car-apps-common/src/com/android/car/apps/common/UxrButton.java b/car-apps-common/src/com/android/car/apps/common/UxrButton.java
index 73520c0..aae5aa3 100644
--- a/car-apps-common/src/com/android/car/apps/common/UxrButton.java
+++ b/car-apps-common/src/com/android/car/apps/common/UxrButton.java
@@ -23,7 +23,7 @@
 import android.os.Handler;
 import android.util.AttributeSet;
 import android.view.View;
-import android.widget.Button;
+import android.widget.TextView;
 import android.widget.Toast;
 
 /**
@@ -34,8 +34,11 @@
  * If not set, it'll use UX_RESTRICTIONS_FULLY_RESTRICTED as fallback.
  * If no restriction is enforced, this Button will work as a normal Button; otherwise, its
  * OnClickListener will be disabled if any, and a blocking message will be displayed.
+ *
+ * This class extends from TextView instead of Button because only TextView supports gradient
+ * truncate for now.
  */
-public class UxrButton extends Button {
+public class UxrButton extends TextView {
     private static final int[] STATE_UX_RESTRICTED = {R.attr.state_ux_restricted};
 
     private CarUxRestrictionsUtil mCarUxRestrictionsUtil;
@@ -119,6 +122,14 @@
         mCarUxRestrictionsUtil.unregister(mListener);
     }
 
+    /**
+     * Set the UX restriction mode for this button
+     */
+    public void setUxRestrictions(int uxRestrictions) {
+        mRestrictions = uxRestrictions;
+        mHandler.post(() -> refreshDrawableState());
+    }
+
     private boolean isRestricted() {
         return CarUxRestrictionsUtil.isRestricted(mRestrictions, mActiveCarUxRestrictions);
     }
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java b/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java
new file mode 100644
index 0000000..f640218
--- /dev/null
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java
@@ -0,0 +1,176 @@
+/*
+ * 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.apps.common.imaging;
+
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.Size;
+
+import com.android.car.apps.common.R;
+import com.android.car.apps.common.UriUtils;
+
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * A helper class to bind an image to a UI element, updating the image when needed.
+ * @param <T> see {@link ImageRef}.
+ */
+public class ImageBinder<T extends ImageBinder.ImageRef> {
+
+    public enum PlaceholderType {
+        /** For elements that don't want to display a placeholder (like tabs). */
+        NONE,
+        /** A placeholder displayed in the foreground, typically has more details. */
+        FOREGROUND,
+        /** A placeholder displayed in the background, typically has less details. */
+        BACKGROUND
+    }
+
+    /**
+     * Interface to define keys for identifying images.
+     */
+    public interface ImageRef {
+
+        /** Returns whether the given {@link ImageRef} and this one reference the same image. */
+        boolean equals(Context context, Object other);
+
+        /** Returns the uri to use to retrieve the image. */
+        @Nullable Uri getImageURI();
+
+        /** For when the image ref doesn't always use a uri. */
+        default @Nullable Drawable getImage(Context context) {
+            return null;
+        }
+
+        /** Returns a placeholder for images that can't be found. */
+        Drawable getPlaceholder(Context context, @NonNull PlaceholderType type);
+    }
+
+    private final PlaceholderType mPlaceholderType;
+    private final Size mMaxImageSize;
+    @Nullable
+    private final Consumer<Drawable> mClient;
+
+    private T mCurrentRef;
+    private ImageKey mCurrentKey;
+    private BiConsumer<ImageKey, Drawable> mFetchReceiver;
+    private Drawable mLoadingDrawable;
+
+
+    public ImageBinder(@NonNull PlaceholderType type, @NonNull Size maxImageSize,
+            @NonNull Consumer<Drawable> consumer) {
+        mPlaceholderType = checkNotNull(type, "Need a type");
+        mMaxImageSize = checkNotNull(maxImageSize, "Need a size");
+        mClient = checkNotNull(consumer, "Cannot bind a null consumer");
+    }
+
+    protected ImageBinder(@NonNull PlaceholderType type, @NonNull Size maxImageSize) {
+        mPlaceholderType = checkNotNull(type, "Need a type");
+        mMaxImageSize = checkNotNull(maxImageSize, "Need a size");
+        mClient = null;
+    }
+
+    protected void setDrawable(@Nullable Drawable drawable) {
+        if (mClient != null) {
+            mClient.accept(drawable);
+        }
+    }
+
+    /** Fetches a new image if needed. */
+    public void setImage(Context context, @Nullable T newRef) {
+        if (isSameImage(context, newRef)) {
+            return;
+        }
+
+        prepareForNewBinding(context);
+
+        mCurrentRef = newRef;
+
+        if (mCurrentRef == null) {
+            setDrawable(null);
+        } else {
+            Drawable image = mCurrentRef.getImage(context);
+            if (image != null) {
+                setDrawable(image);
+                return;
+            }
+
+            mFetchReceiver = (key, drawable) -> {
+                if (Objects.equals(mCurrentKey, key)) {
+                    Drawable displayed =
+                            (drawable == null && mPlaceholderType != PlaceholderType.NONE)
+                                    ? mCurrentRef.getPlaceholder(context, mPlaceholderType)
+                                    : drawable;
+                    setDrawable(displayed);
+                    onRequestFinished();
+                }
+            };
+
+            if (UriUtils.isEmpty(mCurrentRef.getImageURI())) {
+                mCurrentKey = null;
+                mFetchReceiver.accept(null, null);
+            } else {
+                mCurrentKey = new ImageKey(mCurrentRef.getImageURI(), mMaxImageSize);
+                getImageFetcher(context).getImage(context, mCurrentKey, mFetchReceiver);
+            }
+        }
+    }
+
+    private boolean isSameImage(Context context, @Nullable T newRef) {
+        if (mCurrentRef == null && newRef == null) return true;
+
+        if (mCurrentRef != null && newRef != null) {
+            return mCurrentRef.equals(context, newRef);
+        }
+
+        return false;
+    }
+
+    private LocalImageFetcher getImageFetcher(Context context) {
+        return LocalImageFetcher.getInstance(context);
+    }
+
+    protected void prepareForNewBinding(Context context) {
+        if (mCurrentKey != null) {
+            getImageFetcher(context).cancelRequest(mCurrentKey, mFetchReceiver);
+            onRequestFinished();
+        }
+        setDrawable(mPlaceholderType != PlaceholderType.NONE ? getLoadingDrawable(context) : null);
+    }
+
+    private void onRequestFinished() {
+        mCurrentKey = null;
+        mFetchReceiver = null;
+    }
+
+    private Drawable getLoadingDrawable(Context context) {
+        if (mLoadingDrawable == null) {
+            int color = context.getColor(R.color.loading_image_placeholder_color);
+            mLoadingDrawable = new ColorDrawable(color);
+        }
+        return mLoadingDrawable;
+    }
+}
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/ImageKey.java b/car-apps-common/src/com/android/car/apps/common/imaging/ImageKey.java
new file mode 100644
index 0000000..464d974
--- /dev/null
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/ImageKey.java
@@ -0,0 +1,64 @@
+/*
+ * 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.apps.common.imaging;
+
+import android.net.Uri;
+import android.util.Size;
+
+import com.android.car.apps.common.UriUtils;
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/** Class to identify image load requests. */
+@SuppressWarnings("WeakerAccess")
+class ImageKey {
+    public final Uri mImageUri;
+    public final Size mMaxImageSize;
+
+    /** imageUri must NOT be {@link com.android.car.apps.common.UriUtils#isEmpty}*/
+    ImageKey(Uri imageUri, Size maxImageSize) {
+        Preconditions.checkArgument(!UriUtils.isEmpty(imageUri), "Empty uri!");
+        mImageUri = imageUri;
+        mMaxImageSize = maxImageSize;
+    }
+
+    /** Auto generated. */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ImageKey imageKey = (ImageKey) o;
+        return mImageUri.equals(imageKey.mImageUri)
+                && mMaxImageSize.equals(imageKey.mMaxImageSize);
+    }
+
+    /** Auto generated. */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mImageUri, mMaxImageSize);
+    }
+
+    /** Auto generated. */
+    @Override
+    public String toString() {
+        return "ImageKey{"
+                + "mImageUri=" + mImageUri
+                + ", mMaxImageSize=" + mMaxImageSize
+                + '}';
+    }
+}
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
new file mode 100644
index 0000000..ee50b72
--- /dev/null
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
@@ -0,0 +1,127 @@
+/*
+ * 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.apps.common.imaging;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.Size;
+import android.widget.ImageView;
+
+import com.android.car.apps.common.CommonFlags;
+import com.android.car.apps.common.R;
+
+/**
+ * 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> {
+
+    @Nullable
+    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);
+    }
+
+    /**
+     * See {@link ImageViewBinder} and {@link ImageBinder}.
+     * @param flagBitmaps whether this binder should flag bitmap drawables if flagging is enabled.
+     */
+    public ImageViewBinder(PlaceholderType type, Size maxImageSize,
+            @Nullable ImageView imageView, boolean flagBitmaps) {
+        super(type, maxImageSize);
+        mImageView = imageView;
+        mFlagBitmaps = flagBitmaps;
+    }
+
+    @Override
+    protected void setDrawable(@Nullable Drawable drawable) {
+        if (mImageView != null) {
+            mImageView.setImageDrawable(drawable);
+            if (mFlagBitmaps) {
+                CommonFlags flags = CommonFlags.getInstance(mImageView.getContext());
+                if (flags.shouldFlagImproperImageRefs()) {
+                    if (drawable instanceof BitmapDrawable) {
+                        int tint = mImageView.getContext().getColor(
+                                R.color.improper_image_refs_tint_color);
+                        mImageView.setColorFilter(tint);
+                    } else {
+                        mImageView.clearColorFilter();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 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);
+        mImageView.setImageDrawable(null);
+        mImageView.clearColorFilter();
+        // Call super last to setup the default loading drawable.
+        super.prepareForNewBinding(context);
+    }
+
+}
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
new file mode 100644
index 0000000..e8b245e
--- /dev/null
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/LocalImageFetcher.java
@@ -0,0 +1,347 @@
+/*
+ * 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.apps.common.imaging;
+
+import android.annotation.UiThread;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.ImageDecoder;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+import android.util.LruCache;
+
+import com.android.car.apps.common.BitmapUtils;
+import com.android.car.apps.common.CommonFlags;
+import com.android.car.apps.common.R;
+import com.android.car.apps.common.UriUtils;
+import com.android.car.apps.common.util.CarAppsIOUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.function.BiConsumer;
+
+
+/**
+ * A singleton that fetches images and offers a simple memory cache. The requests and the replies
+ * all happen on the UI thread.
+ */
+public class LocalImageFetcher {
+
+    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;
+
+    /** Should not be reset to null once created. */
+    private static LocalImageFetcher sInstance;
+
+    /** Returns the singleton. */
+    public static LocalImageFetcher getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new LocalImageFetcher(context);
+        }
+        return sInstance;
+    }
+
+    private final int mPoolSize;
+
+    private final LruCache<String, Executor> mThreadPools;
+
+    private final Map<ImageKey, HashSet<BiConsumer<ImageKey, Drawable>>> mConsumers =
+            new HashMap<>(20);
+    private final Map<ImageKey, ImageLoadingTask> mTasks = new HashMap<>(20);
+
+    private final LruCache<ImageKey, Drawable> mMemoryCache;
+
+    private final boolean mFlagRemoteImages;
+
+    @UiThread
+    private LocalImageFetcher(Context context) {
+        Resources res = context.getResources();
+        int maxPools = res.getInteger(R.integer.image_fetcher_thread_pools_max_count);
+        mPoolSize = res.getInteger(R.integer.image_fetcher_thread_pool_size);
+        mThreadPools = new LruCache<>(maxPools);
+
+        int cacheSizeMB = res.getInteger(R.integer.bitmap_memory_cache_max_size_mb);
+        int drawableDefaultWeightKB = res.getInteger(R.integer.drawable_default_weight_kb);
+        mMemoryCache = new LruCache<ImageKey, Drawable>(cacheSizeMB * MB) {
+            @Override
+            protected int sizeOf(ImageKey key, Drawable drawable) {
+                if (drawable instanceof BitmapDrawable) {
+                    return ((BitmapDrawable) drawable).getBitmap().getAllocationByteCount();
+                } else {
+                    // For now
+                    // TODO(b/139386940): consider a more accurate sizing / caching strategy.
+                    return drawableDefaultWeightKB * KB;
+                }
+            }
+        };
+
+        mFlagRemoteImages = CommonFlags.getInstance(context).shouldFlagImproperImageRefs();
+    }
+
+    private Executor getThreadPool(String packageName) {
+        Executor result = mThreadPools.get(packageName);
+        if (result == null) {
+            result = Executors.newFixedThreadPool(mPoolSize);
+            mThreadPools.put(packageName, result);
+        }
+        return result;
+    }
+
+    /** Fetches an image. The resulting drawable may be null. */
+    @UiThread
+    public void getImage(Context context, ImageKey key, BiConsumer<ImageKey, Drawable> consumer) {
+        Drawable cached = mMemoryCache.get(key);
+        if (cached != null) {
+            consumer.accept(key, cached);
+            return;
+        }
+
+        ImageLoadingTask task = mTasks.get(key);
+
+        HashSet<BiConsumer<ImageKey, Drawable>> consumers = mConsumers.get(key);
+        if (consumers == null) {
+            consumers = new HashSet<>(3);
+            if (task != null && L_WARN) {
+                Log.w(TAG, "Expected no task here for " + key);
+            }
+            mConsumers.put(key, consumers);
+        }
+        consumers.add(consumer);
+
+        if (task == null) {
+            String packageName = UriUtils.getPackageName(context, key.mImageUri);
+            if (packageName != null) {
+                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);
+            }
+        }
+    }
+
+    /** Cancels a request made via {@link #getImage}. */
+    @UiThread
+    public void cancelRequest(ImageKey key, BiConsumer<ImageKey, Drawable> consumer) {
+        HashSet<BiConsumer<ImageKey, Drawable>> consumers = mConsumers.get(key);
+        if (consumers != null) {
+            boolean removed = consumers.remove(consumer);
+            if (consumers.isEmpty()) {
+                // Nobody else wants this image, remove the set and cancel the task.
+                mConsumers.remove(key);
+                ImageLoadingTask task = mTasks.remove(key);
+                if (task != null) {
+                    task.cancel(true);
+                    if (L_DEBUG) {
+                        Log.d(TAG, "Canceled task " + key.mImageUri);
+                    }
+                } else if (L_WARN) {
+                    Log.w(TAG, "cancelRequest missing task for: " + key);
+                }
+            }
+
+            if (!removed && L_WARN) {
+                Log.w(TAG, "cancelRequest missing consumer for: " + key);
+            }
+        } else if (L_WARN) {
+            Log.w(TAG, "cancelRequest has no consumers for: " + key);
+        }
+    }
+
+
+    @UiThread
+    private void fulfilRequests(ImageLoadingTask task, Drawable drawable) {
+        ImageKey key = task.mImageKey;
+        ImageLoadingTask pendingTask = mTasks.get(key);
+        if (pendingTask == task) {
+            if (drawable != null) {
+                mMemoryCache.put(key, drawable);
+            }
+
+            HashSet<BiConsumer<ImageKey, Drawable>> consumers = mConsumers.remove(key);
+            mTasks.remove(key);
+            if (consumers != null) {
+                for (BiConsumer<ImageKey, Drawable> consumer : consumers) {
+                    consumer.accept(key, drawable);
+                }
+            }
+        } else if (L_WARN) {
+            // This case would possible if a running task was canceled, a new one was restarted
+            // right away for the same key, and the canceled task still managed to call
+            // fulfilRequests (despite the !isCancelled check).
+            Log.w(TAG, "A new task already started for: " + task.mImageKey);
+        }
+    }
+
+
+    private static class ImageLoadingTask extends AsyncTask<Void, Void, Drawable> {
+
+        private final WeakReference<Context> mWeakContext;
+        private final ImageKey mImageKey;
+        private final boolean mFlagRemoteImages;
+
+
+        @UiThread
+        ImageLoadingTask(Context context, ImageKey request, boolean flagRemoteImages) {
+            mWeakContext = new WeakReference<>(context.getApplicationContext());
+            mImageKey = request;
+            mFlagRemoteImages = flagRemoteImages;
+        }
+
+        /** Runs in the background. */
+        private final ImageDecoder.OnHeaderDecodedListener mOnHeaderDecodedListener =
+                new ImageDecoder.OnHeaderDecodedListener() {
+            @Override
+            public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
+                    ImageDecoder.Source source) {
+                if (isCancelled()) throw new CancellationException();
+                decoder.setAllocator(mAllocatorMode);
+                int maxW = mImageKey.mMaxImageSize.getWidth();
+                int maxH = mImageKey.mMaxImageSize.getHeight();
+                int imgW = info.getSize().getWidth();
+                int imgH = info.getSize().getHeight();
+                if (imgW > maxW || imgH > maxH) {
+                    float scale = Math.min(maxW / (float) imgW, maxH / (float) imgH);
+                    decoder.setTargetSize(Math.round(scale * imgW), Math.round(scale * imgH));
+                }
+            }
+        };
+
+        // ALLOCATOR_HARDWARE causes crashes on some emulators (in media center's queue).
+        private @ImageDecoder.Allocator int mAllocatorMode = ImageDecoder.ALLOCATOR_SOFTWARE;
+
+        @Override
+        protected Drawable doInBackground(Void... voids) {
+            try {
+                if (isCancelled()) return null;
+                Uri imageUri = mImageKey.mImageUri;
+
+                Context context = mWeakContext.get();
+                if (context == null) return null;
+
+                if (UriUtils.isAndroidResourceUri(imageUri)) {
+                    // ImageDecoder doesn't support all resources via the content provider...
+                    return UriUtils.getDrawable(context,
+                            UriUtils.getIconResource(context, imageUri));
+                } else if (UriUtils.isContentUri(imageUri)) {
+                    ContentResolver resolver = context.getContentResolver();
+
+                    // TODO(b/140959390): Remove the check once the bug is fixed in framework.
+                    if (!hasFile(resolver, imageUri)) {
+                        if (L_WARN) {
+                            Log.w(TAG, "File not found in uri: " + imageUri);
+                        }
+                        return null;
+                    }
+
+                    ImageDecoder.Source src = ImageDecoder.createSource(resolver, imageUri);
+                    return ImageDecoder.decodeDrawable(src, mOnHeaderDecodedListener);
+
+                } else if (mFlagRemoteImages) {
+                    mAllocatorMode = ImageDecoder.ALLOCATOR_SOFTWARE; // Needed for canvas drawing.
+                    URL url = new URL(imageUri.toString());
+
+                    try (InputStream is = new BufferedInputStream(url.openStream());
+                         ByteArrayOutputStream bytes = new ByteArrayOutputStream()) {
+
+                        CarAppsIOUtils.copy(is, bytes);
+                        ImageDecoder.Source src = ImageDecoder.createSource(bytes.toByteArray());
+                        Bitmap decoded = ImageDecoder.decodeBitmap(src, mOnHeaderDecodedListener);
+                        Bitmap tinted = BitmapUtils.createTintedBitmap(decoded,
+                                context.getColor(R.color.improper_image_refs_tint_color));
+                        return new BitmapDrawable(context.getResources(), tinted);
+                    }
+                }
+            } catch (IOException ioe) {
+                Log.e(TAG, "ImageLoadingTask#doInBackground: " + ioe);
+            } catch (CancellationException e) {
+                return null;
+            }
+            return null;
+        }
+
+        private boolean hasFile(ContentResolver resolver, Uri uri) {
+            AssetFileDescriptor assetFd = null;
+            try {
+                if (uri.getScheme() == ContentResolver.SCHEME_CONTENT) {
+                    assetFd = resolver.openTypedAssetFileDescriptor(uri, "image/*", null);
+                } else {
+                    assetFd = resolver.openAssetFileDescriptor(uri, "r");
+                }
+            } catch (FileNotFoundException e) {
+                // Some images cannot be opened as AssetFileDescriptors (e.g.bmp, ico). Open them
+                // as InputStreams.
+                try {
+                    InputStream is = resolver.openInputStream(uri);
+                    if (is != null) {
+                        IoUtils.closeQuietly(is);
+                        return true;
+                    }
+                } catch (IOException exception) {
+                    return false;
+                }
+            }
+            if (assetFd != null) {
+                IoUtils.closeQuietly(assetFd);
+                return true;
+            }
+            return false;
+        }
+
+        @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);
+                } else {
+                    Log.e(TAG, "ImageLoadingTask#onPostExecute: LocalImageFetcher was reset !");
+                }
+            }
+        }
+    }
+}
diff --git a/car-apps-common/src/com/android/car/apps/common/util/CarAppsIOUtils.java b/car-apps-common/src/com/android/car/apps/common/util/CarAppsIOUtils.java
new file mode 100644
index 0000000..fb10f49
--- /dev/null
+++ b/car-apps-common/src/com/android/car/apps/common/util/CarAppsIOUtils.java
@@ -0,0 +1,37 @@
+/*
+ * 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.apps.common.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Misc IO utilities. */
+public class CarAppsIOUtils {
+
+    /** The default buffer size used in this class. */
+    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
+
+    /** Copies the data from one stream to the other. */
+    public static void copy(InputStream input, OutputStream output) throws IOException {
+        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+        int n;
+        while (-1 != (n = input.read(buffer))) {
+            output.write(buffer, 0, n);
+        }
+    }
+}
diff --git a/car-apps-common/src/com/android/car/apps/common/util/CarPackageManagerUtils.java b/car-apps-common/src/com/android/car/apps/common/util/CarPackageManagerUtils.java
new file mode 100644
index 0000000..43e4381
--- /dev/null
+++ b/car-apps-common/src/com/android/car/apps/common/util/CarPackageManagerUtils.java
@@ -0,0 +1,87 @@
+/*
+ * 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.apps.common.util;
+
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.content.pm.CarPackageManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Utility class to access CarPackageManager
+ */
+public class CarPackageManagerUtils {
+    private static final String TAG = "CarPackageManagerUtils";
+
+    private final Car mCarApi;
+    private CarPackageManager mCarPackageManager;
+
+    private static CarPackageManagerUtils sInstance = null;
+
+    private CarPackageManagerUtils(Context context) {
+        mCarApi = Car.createCar(context.getApplicationContext());
+        try {
+            mCarPackageManager = (CarPackageManager) mCarApi.getCarManager(Car.PACKAGE_SERVICE);
+        } catch (CarNotConnectedException e) {
+            Log.e(TAG, "Car not connected when retrieving car package manager", e);
+        }
+    }
+
+    /**
+     * Returns the singleton instance of this class
+     */
+    @NonNull
+    public static CarPackageManagerUtils getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new CarPackageManagerUtils(context);
+        }
+        return sInstance;
+    }
+
+    /**
+     * Returns true if the provided Activity is distraction optimized
+     */
+    public boolean isDistractionOptimized(@NonNull ActivityInfo activityInfo) {
+        if (mCarPackageManager != null) {
+            try {
+                return mCarPackageManager.isActivityDistractionOptimized(
+                        activityInfo.packageName, activityInfo.name);
+            } catch (CarNotConnectedException e) {
+                Log.e(TAG, "Car not connected when getting driver optimization info", e);
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Attempts to resolve the provided intent into an activity, and returns true if the
+     * resolved activity is distraction optimized
+     */
+    public boolean isDistractionOptimized(PackageManager packageManager, Intent intent) {
+        ResolveInfo info = packageManager.resolveActivity(
+                intent, PackageManager.MATCH_DEFAULT_ONLY);
+        return (info != null) ? isDistractionOptimized(info.activityInfo) : false;
+    }
+}
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 5baf4be..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
@@ -19,12 +19,17 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.annotation.NonNull;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.view.View;
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Utility methods to operate over views.
  */
@@ -63,6 +68,20 @@
     }
 
     /**
+     * Hides views using a fade-out animation
+     *
+     * @param views    {@link View}s to be hidden
+     * @param duration animation duration in milliseconds.
+     */
+    public static void hideViewsAnimated(@Nullable List<View> views, int duration) {
+        for (View view : views) {
+            if (view != null) {
+                hideViewAnimated(view, duration);
+            }
+        }
+    }
+
+    /**
      * Shows a view using a fade-in animation
      *
      * @param view     {@link View} to be shown
@@ -87,6 +106,20 @@
                 .alpha(1f);
     }
 
+    /**
+     * Shows views using a fade-out animation
+     *
+     * @param views    {@link View}s to be shown.
+     * @param duration animation duration in milliseconds.
+     */
+    public static void showViewsAnimated(@Nullable List<View> views, int duration) {
+        for (View view : views) {
+            if (view != null) {
+                showViewAnimated(view, duration);
+            }
+        }
+    }
+
     /** Sets the visibility of the (optional) view to {@link View#VISIBLE} or {@link View#GONE}. */
     public static void setVisible(@Nullable View view, boolean visible) {
         if (view != null) {
@@ -94,6 +127,13 @@
         }
     }
 
+    /** Sets the visibility of the views to {@link View#VISIBLE} or {@link View#GONE}. */
+    public static void setVisible(@Nullable List<View> views, boolean visible) {
+        for (View view : views) {
+            setVisible(view, visible);
+        }
+    }
+
     /**
      * Sets the visibility of the (optional) view to {@link View#INVISIBLE} or {@link View#VISIBLE}.
      */
@@ -116,4 +156,49 @@
             view.setText(text);
         }
     }
+
+    /** Sets the enabled state of the (optional) view. */
+    public static void setEnabled(@Nullable View view, boolean enabled) {
+        if (view != null) {
+            view.setEnabled(enabled);
+        }
+    }
+
+    /** 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) {
+            view.setOnClickListener(l);
+        }
+    }
+
+    /** Helper interface for {@link #getViewsById(View, Resources, int, Filter)} getViewsById}. */
+    public interface Filter {
+        /** Returns whether a view should be added to the returned List. */
+        boolean isValid(View view);
+    }
+
+    /** Get views from typed array. */
+    public static List<View> getViewsById(@NonNull View root, @NonNull Resources res, int arrayId,
+            @Nullable Filter filter) {
+        TypedArray viewIds = res.obtainTypedArray(arrayId);
+        List<View> views = new ArrayList<>(viewIds.length());
+        for (int i = 0; i < viewIds.length(); i++) {
+            int viewId = viewIds.getResourceId(i, 0);
+            if (viewId != 0) {
+                View view = root.findViewById(viewId);
+                if (view != null && (filter == null || filter.isValid(view))) {
+                    views.add(view);
+                }
+            }
+        }
+        viewIds.recycle();
+        return views;
+    }
 }
diff --git a/car-apps-common/src/com/android/car/apps/common/widget/CarTabLayout.java b/car-apps-common/src/com/android/car/apps/common/widget/CarTabLayout.java
index bf98521..4dd427f 100644
--- a/car-apps-common/src/com/android/car/apps/common/widget/CarTabLayout.java
+++ b/car-apps-common/src/com/android/car/apps/common/widget/CarTabLayout.java
@@ -34,6 +34,7 @@
 import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
 
 import com.android.car.apps.common.R;
 import com.android.car.apps.common.util.Themes;
@@ -59,7 +60,9 @@
  * <p>Touch feedback is using @android:attr/selectableItemBackground.
  *
  * @param <T> Presents a CarTab entity
+ * @deprecated Use {@link com.android.car.ui.TabLayout} instead
  */
+@Deprecated
 public class CarTabLayout<T extends CarTabLayout.CarTab> extends LinearLayout {
 
     /**
@@ -231,7 +234,6 @@
     }
 
     private static class CarTabAdapter<T extends CarTab> extends BaseAdapter {
-        private static final int MEDIUM_WEIGHT = 500;
         private final Context mContext;
         private final CarTabLayout mCarTabLayout;
         @LayoutRes
@@ -245,9 +247,8 @@
             mContext = context;
             mCarTabItemLayoutRes = res;
             mCarTabLayout = carTabLayout;
-            mUnselectedTypeface = Typeface.defaultFromStyle(Typeface.NORMAL);
-            // TODO: add indirection to allow customization.
-            mSelectedTypeface = Typeface.create(mUnselectedTypeface, MEDIUM_WEIGHT, false);
+            mUnselectedTypeface = createStyledTypeface(context, R.style.CarTabItemText);
+            mSelectedTypeface = createStyledTypeface(context, R.style.CarTabSelectedTextTypeface);
         }
 
         private void add(@NonNull T carTab) {
@@ -341,6 +342,16 @@
             textView.setSelected(carTab.mIsSelected);
             textView.setTypeface(carTab.mIsSelected ? mSelectedTypeface : mUnselectedTypeface);
         }
+
+        private static Typeface createStyledTypeface(Context context, @StyleRes int styleResId) {
+            // If not specified, default to 0, which stands for normal.
+            int textStyle = Themes.getAttrInteger(context, styleResId, android.R.attr.textStyle);
+            // If not specified, default value will be 0 which is a light font.
+            int textFontWeight = Themes.getAttrInteger(context, styleResId,
+                    android.R.attr.textFontWeight);
+            return Typeface.create(Typeface.defaultFromStyle(textStyle), textFontWeight,
+                    (textStyle & Typeface.ITALIC) != 0);
+        }
     }
 
     /** Car tab entity. */
@@ -359,7 +370,7 @@
             textView.setText(mText);
         }
 
-        /** Set icon drawable. */
+        /** Set icon drawable. TODO(b/139444064): revise this api. */
         protected void bindIcon(ImageView imageView) {
             imageView.setImageDrawable(mIcon);
         }
diff --git a/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerView.java b/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerView.java
index 08ba31b..2917967 100644
--- a/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerView.java
+++ b/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerView.java
@@ -21,8 +21,11 @@
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
 import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.View;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 
@@ -49,8 +52,6 @@
     private static final boolean DEBUG = false;
     private static final String TAG = "PagedRecyclerView";
 
-    private Context mContext;
-
     private final CarUxRestrictionsUtil mCarUxRestrictionsUtil;
     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener;
 
@@ -227,26 +228,19 @@
             return;
         }
 
-        mContext = context;
-        mNestedRecyclerView = new RecyclerView(mContext, attrs,
+        mNestedRecyclerView = new RecyclerView(context, attrs,
                 R.style.PagedRecyclerView_NestedRecyclerView);
 
-        PagedRecyclerViewLayoutManager layoutManager = new PagedRecyclerViewLayoutManager(context);
-        super.setLayoutManager(layoutManager);
-
-        PagedRecyclerViewAdapter adapter = new PagedRecyclerViewAdapter();
-        super.setAdapter(adapter);
-
+        super.setLayoutManager(new PagedRecyclerViewLayoutManager(context));
+        super.setAdapter(new PagedRecyclerViewAdapter());
         super.setNestedScrollingEnabled(false);
         super.setClipToPadding(false);
 
         // Gutter
-        int defaultGutterSize = getResources().getDimensionPixelSize(R.dimen.car_scroll_bar_margin);
         mGutter = a.getInt(R.styleable.PagedRecyclerView_gutter, Gutter.BOTH);
-        mGutterSize = defaultGutterSize;
+        mGutterSize = getResources().getDimensionPixelSize(R.dimen.car_scroll_bar_margin);
 
-        int carMargin = mContext.getResources().getDimensionPixelSize(
-                R.dimen.car_scroll_bar_margin);
+        int carMargin = getResources().getDimensionPixelSize(R.dimen.car_scroll_bar_margin);
         mScrollBarContainerWidth = a.getDimensionPixelSize(
                 R.styleable.PagedRecyclerView_scrollBarContainerWidth, carMargin);
 
@@ -516,7 +510,9 @@
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     public void layoutBothForTesting(int l, int t, int r, int b) {
         super.layout(l, t, r, b);
-        mNestedRecyclerView.layout(l, t, r, b);
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.layout(l, t, r, b);
+        }
     }
 
     @Override
@@ -562,14 +558,14 @@
     private void createScrollBarFromConfig() {
         if (DEBUG) Log.d(TAG, "createScrollBarFromConfig");
         final String clsName = mScrollBarClass == null
-                ? mContext.getString(R.string.config_scrollBarComponent) : mScrollBarClass;
+                ? getContext().getString(R.string.config_scrollBarComponent) : mScrollBarClass;
         if (clsName == null || clsName.length() == 0) {
             throw andLog("No scroll bar component configured", null);
         }
 
         Class<?> cls;
         try {
-            cls = mContext.getClassLoader().loadClass(clsName);
+            cls = getContext().getClassLoader().loadClass(clsName);
         } catch (Throwable t) {
             throw andLog("Error loading scroll bar component: " + clsName, t);
         }
@@ -579,7 +575,7 @@
             throw andLog("Error creating scroll bar component: " + clsName, t);
         }
 
-        mScrollBarUI.initialize(mContext, mNestedRecyclerView, mScrollBarContainerWidth,
+        mScrollBarUI.initialize(getContext(), mNestedRecyclerView, mScrollBarContainerWidth,
                 mScrollBarPosition, mScrollBarAboveRecyclerView);
 
         mScrollBarUI.setPadding(mScrollBarPaddingStart, mScrollBarPaddingEnd);
@@ -639,4 +635,61 @@
         Log.e(TAG, msg, t);
         throw new RuntimeException(msg, t);
     }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState, getContext());
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.saveHierarchyState(ss.mNestedRecyclerViewState);
+        }
+        return ss;
+    }
+
+    @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);
+            }
+        }
+    }
+
+    static class SavedState extends BaseSavedState {
+        SparseArray mNestedRecyclerViewState;
+        Context mContext;
+
+        SavedState(Parcelable superState, Context c) {
+            super(superState);
+            mContext = c;
+            mNestedRecyclerViewState = new SparseArray();
+        }
+
+        private SavedState(Parcel source, ClassLoader loader) {
+            super(source, loader);
+            mNestedRecyclerViewState = source.readSparseArray(loader);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeSparseArray(mNestedRecyclerViewState);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in, getClass().getClassLoader());
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
 }
diff --git a/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerViewAdapter.java b/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerViewAdapter.java
index 623a3d4..bc35a37 100644
--- a/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerViewAdapter.java
+++ b/car-apps-common/src/com/android/car/apps/common/widget/PagedRecyclerViewAdapter.java
@@ -25,18 +25,11 @@
 
 import com.android.car.apps.common.R;
 
-import java.util.ArrayList;
-
 /**
  * The adapter for the parent recyclerview in {@link PagedRecyclerView} widget.
  */
 final class PagedRecyclerViewAdapter
         extends RecyclerView.Adapter<PagedRecyclerViewAdapter.NestedRowViewHolder> {
-    private ArrayList<String> mItem = new ArrayList<>();
-
-    PagedRecyclerViewAdapter() {
-        this.mItem.add("nested_RecyclerView");
-    }
 
     @Override
     public PagedRecyclerViewAdapter.NestedRowViewHolder onCreateViewHolder(ViewGroup parent,
@@ -55,7 +48,7 @@
     // Return the size of your dataset (invoked by the layout manager)
     @Override
     public int getItemCount() {
-        return mItem.size();
+        return 1;
     }
 
     /**
diff --git a/car-arch-common/src/com/android/car/arch/common/preference/PreferenceLiveData.java b/car-arch-common/src/com/android/car/arch/common/preference/PreferenceLiveData.java
deleted file mode 100644
index c9a9972..0000000
--- a/car-arch-common/src/com/android/car/arch/common/preference/PreferenceLiveData.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.arch.common.preference;
-
-import android.content.SharedPreferences;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.LiveData;
-
-import java.util.Objects;
-
-/**
- * A LiveData that populates its value from {@link SharedPreferences}. When active, this LiveData
- * will listen for changes to the supplied key and update its value.
- *
- * @param <T> The type this PreferenceLiveData will emit.
- */
-public abstract class PreferenceLiveData<T> extends LiveData<T> {
-
-    private final SharedPreferences mPreferences;
-    private final String mKey;
-
-    private final SharedPreferences.OnSharedPreferenceChangeListener mListener =
-            (sharedPreferences, key) -> {
-                if (Objects.equals(key, PreferenceLiveData.this.mKey)) {
-                    setValue(fetchValue(sharedPreferences, key));
-                }
-            };
-
-    public PreferenceLiveData(
-            @NonNull SharedPreferences preferences, @NonNull String key) {
-        mPreferences = preferences;
-        mKey = key;
-    }
-
-    /**
-     * Subclasses should extract the required value from {@code preferences} corresponding to the
-     * given {@code key} and return it. Subclasses should not directly call {@link #setValue(T)}
-     * since it will be called when appropriate.
-     */
-    protected abstract T fetchValue(@NonNull SharedPreferences preferences, @NonNull String key);
-
-    @Override
-    protected void onActive() {
-        super.onActive();
-        setValue(fetchValue(mPreferences, mKey));
-        mPreferences.registerOnSharedPreferenceChangeListener(mListener);
-    }
-
-    @Override
-    protected void onInactive() {
-        super.onInactive();
-        mPreferences.unregisterOnSharedPreferenceChangeListener(mListener);
-    }
-}
diff --git a/car-arch-common/src/com/android/car/arch/common/preference/StringPreferenceLiveData.java b/car-arch-common/src/com/android/car/arch/common/preference/StringPreferenceLiveData.java
deleted file mode 100644
index 79f56ac..0000000
--- a/car-arch-common/src/com/android/car/arch/common/preference/StringPreferenceLiveData.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.arch.common.preference;
-
-import android.content.SharedPreferences;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * A LiveData whose value is backed by a String preference in a {@link SharedPreferences}
- */
-public class StringPreferenceLiveData extends PreferenceLiveData<String> {
-
-    private final String mDefaultValue;
-
-    /**
-     * Creates a new StringPreferenceLiveData.
-     *
-     * @param preferences  The SharedPreferences to fetch data from.
-     * @param key          The String key to find the data.
-     * @param defaultValue The value to emit when {@code preferences} does not contain data for the
-     *                     given key
-     */
-    public StringPreferenceLiveData(
-            @NonNull SharedPreferences preferences, @NonNull String key,
-            @Nullable String defaultValue) {
-        super(preferences, key);
-        mDefaultValue = defaultValue;
-    }
-
-    @Override
-    protected String fetchValue(@NonNull SharedPreferences preferences, @NonNull String key) {
-        return preferences.getString(key, mDefaultValue);
-    }
-}
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/BundleBuilder.java b/car-assist-client-lib/src/com/android/car/assist/client/BundleBuilder.java
index 742eca6..9428247 100644
--- a/car-assist-client-lib/src/com/android/car/assist/client/BundleBuilder.java
+++ b/car-assist-client-lib/src/com/android/car/assist/client/BundleBuilder.java
@@ -17,6 +17,7 @@
 
 import static com.android.car.assist.CarVoiceInteractionSession.KEY_ACTION;
 import static com.android.car.assist.CarVoiceInteractionSession.KEY_EXCEPTION;
+import static com.android.car.assist.CarVoiceInteractionSession.KEY_FALLBACK_ASSISTANT_ENABLED;
 import static com.android.car.assist.CarVoiceInteractionSession.KEY_NOTIFICATION;
 import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_HANDLE_EXCEPTION;
 import static com.android.car.assist.CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION;
@@ -66,10 +67,12 @@
      * @return The bundle that can be sent to Assistant.
      */
     static Bundle buildAssistantHandleExceptionBundle(
-            @ExceptionValue String exception) {
+            @ExceptionValue String exception,
+            boolean fallbackAssistantEnabled) {
         Bundle args = new Bundle();
         args.putString(KEY_ACTION, VOICE_ACTION_HANDLE_EXCEPTION);
         args.putString(KEY_EXCEPTION, exception);
+        args.putBoolean(KEY_FALLBACK_ASSISTANT_ENABLED, fallbackAssistantEnabled);
         return args;
     }
 }
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
index 7ee2d38..1f9d915 100644
--- a/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
+++ b/car-assist-client-lib/src/com/android/car/assist/client/CarAssistUtils.java
@@ -163,7 +163,8 @@
     public static boolean isCarCompatibleMessagingNotification(StatusBarNotification sbn) {
         return hasMessagingStyle(sbn)
                 && hasRequiredAssistantCallbacks(sbn)
-                && replyCallbackHasRemoteInput(sbn)
+                && ((getReplyAction(sbn.getNotification()) == null)
+                    || replyCallbackHasRemoteInput(sbn))
                 && assistantCallbacksShowNoUi(sbn);
     }
 
@@ -226,6 +227,21 @@
     }
 
     /**
+     * Retrieves the {@link NotificationCompat.Action} containing the
+     * {@link NotificationCompat.Action#SEMANTIC_ACTION_REPLY} semantic action.
+     */
+    @Nullable
+    private static NotificationCompat.Action getReplyAction(Notification notification) {
+        for (NotificationCompat.Action action : getAllActions(notification)) {
+            if (action.getSemanticAction()
+                    == NotificationCompat.Action.SEMANTIC_ACTION_REPLY) {
+                return action;
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns true if the reply callback has at least one {@link RemoteInput}.
      * <p/>
      * Precondition: There exists only one reply callback.
@@ -330,7 +346,8 @@
                 // If there is an active assistant, alert them to request permissions.
                 Bundle handleExceptionBundle = BundleBuilder
                         .buildAssistantHandleExceptionBundle(
-                                EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING);
+                                EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING,
+                                /* fallbackAssistantEnabled */ false);
                 fireAssistantAction(CarVoiceInteractionSession.VOICE_ACTION_HANDLE_EXCEPTION,
                         handleExceptionBundle, callback);
             }
@@ -374,8 +391,13 @@
                 final String fallbackActionResult = hasError ? ActionRequestCallback.RESULT_FAILED
                         : ActionRequestCallback.RESULT_SUCCESS;
                 if (hasActiveAssistant()) {
+                    // If there is an active assistant, alert them to request permissions.
+                    Bundle handleExceptionBundle = BundleBuilder
+                            .buildAssistantHandleExceptionBundle(
+                                    EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING,
+                                    /* fallbackAssistantEnabled */ true);
                     fireAssistantAction(CarVoiceInteractionSession.VOICE_ACTION_HANDLE_EXCEPTION,
-                            null, new ActionRequestCallback() {
+                            handleExceptionBundle, new ActionRequestCallback() {
                                 @Override
                                 public void onResult(String requestActionFromAssistantResult) {
                                     if (fallbackActionResult.equals(
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-assist-client-lib/src/com/android/car/assist/client/tts/AndroidTextToSpeechEngine.java b/car-assist-client-lib/src/com/android/car/assist/client/tts/AndroidTextToSpeechEngine.java
index 31a513d..86f880f 100644
--- a/car-assist-client-lib/src/com/android/car/assist/client/tts/AndroidTextToSpeechEngine.java
+++ b/car-assist-client-lib/src/com/android/car/assist/client/tts/AndroidTextToSpeechEngine.java
@@ -17,6 +17,7 @@
 package com.android.car.assist.client.tts;
 
 import android.content.Context;
+import android.media.AudioAttributes;
 import android.os.Bundle;
 import android.speech.tts.TextToSpeech;
 import android.speech.tts.UtteranceProgressListener;
@@ -51,6 +52,12 @@
     }
 
     @Override
+    public void setAudioAttributes(AudioAttributes audioAttributes) {
+        assertInit();
+        mTextToSpeech.setAudioAttributes(audioAttributes);
+    }
+
+    @Override
     public int speak(CharSequence text, int queueMode, Bundle params, String utteranceId)
             throws IllegalStateException {
         assertInit();
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechEngine.java b/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechEngine.java
index ddcedda..a680cd1 100644
--- a/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechEngine.java
+++ b/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechEngine.java
@@ -17,6 +17,7 @@
 package com.android.car.assist.client.tts;
 
 import android.content.Context;
+import android.media.AudioAttributes;
 import android.os.Bundle;
 import android.speech.tts.TextToSpeech;
 import android.speech.tts.UtteranceProgressListener;
@@ -46,6 +47,14 @@
     void setOnUtteranceProgressListener(UtteranceProgressListener progressListener);
 
     /**
+     * Sets the audio attributes to be used when speaking text or playing
+     * back a file.
+     *
+     * @see TextToSpeech#setAudioAttributes(AudioAttributes)
+     */
+    void setAudioAttributes(AudioAttributes audioAttributes);
+
+    /**
      * Speaks out the provided text.
      *
      * @see TextToSpeech#speak(CharSequence, int, Bundle, String)
diff --git a/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechHelper.java b/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechHelper.java
index e0f985e..c22e832 100644
--- a/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechHelper.java
+++ b/car-assist-client-lib/src/com/android/car/assist/client/tts/TextToSpeechHelper.java
@@ -125,6 +125,7 @@
             }
             mTextToSpeechEngine.initialize(mContext, this::handleInitCompleted);
             mTextToSpeechEngine.setOnUtteranceProgressListener(mProgressListener);
+            mTextToSpeechEngine.setAudioAttributes(mAudioAttributes);
         }
         // Since we're handling a request, delay engine shutdown.
         mHandler.removeCallbacks(mMaybeShutdownRunnable);
diff --git a/car-assist-lib/src/com/android/car/assist/CarVoiceInteractionSession.java b/car-assist-lib/src/com/android/car/assist/CarVoiceInteractionSession.java
index cd58229..6c16fdc 100644
--- a/car-assist-lib/src/com/android/car/assist/CarVoiceInteractionSession.java
+++ b/car-assist-lib/src/com/android/car/assist/CarVoiceInteractionSession.java
@@ -42,6 +42,13 @@
     public static final String KEY_EXCEPTION = "KEY_EXCEPTION";
 
     /**
+     * The key used for the {@link CarVoiceInteractionSession#VOICE_ACTION_HANDLE_EXCEPTION} payload
+     * {@link Bundle}. Must map to a boolean. If value is true, the Fallback Assistant that can
+     * handle the user's request has been disabled.
+     */
+    public static final String KEY_FALLBACK_ASSISTANT_ENABLED = "KEY_FALLBACK_ASSISTANT_ENABLED";
+
+    /**
      * The key used for the payload {@link Bundle}, if a {@link StatusBarNotification} is used as
      * the payload.
      */
diff --git a/car-broadcastradio-support/res/values-or/strings.xml b/car-broadcastradio-support/res/values-or/strings.xml
index 2a149ad..ab448c8 100644
--- a/car-broadcastradio-support/res/values-or/strings.xml
+++ b/car-broadcastradio-support/res/values-or/strings.xml
@@ -20,5 +20,5 @@
     <string name="radio_fm_text" msgid="1973045042281933494">"FM"</string>
     <string name="radio_dab_text" msgid="8456449462266648979">"DAB"</string>
     <string name="program_list_text" msgid="4414150317304422313">"ଷ୍ଟେଶନ୍"</string>
-    <string name="favorites_list_text" msgid="7829827713977109155">"ପସନ୍ଦଦାର୍"</string>
+    <string name="favorites_list_text" msgid="7829827713977109155">"ପସନ୍ଦର"</string>
 </resources>
diff --git a/car-media-common/Android.mk b/car-media-common/Android.mk
index 11bab19..c5041c4 100644
--- a/car-media-common/Android.mk
+++ b/car-media-common/Android.mk
@@ -40,9 +40,6 @@
     car-arch-common
 
 LOCAL_STATIC_JAVA_LIBRARIES += \
-    car-media-common-glide-target \
-    car-media-common-gifdecoder-target \
-    car-media-common-disklrucache-target \
     androidx-constraintlayout_constraintlayout-solver
 
 LOCAL_USE_AAPT2 := true
diff --git a/car-media-common/res/anim/media_app_selector_fade_out.xml b/car-media-common/res/anim/media_app_selector_fade_out.xml
deleted file mode 100644
index 4a1ddf6..0000000
--- a/car-media-common/res/anim/media_app_selector_fade_out.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-    Copyright 2017 The Android Open Source Project
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
-    <alpha
-        android:duration="@android:integer/config_mediumAnimTime"
-        android:fromAlpha="1.0"
-        android:toAlpha="0.2"/>
-</set>
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/drawable/circular_progress_bar.xml b/car-media-common/res/drawable/circular_progress_bar.xml
new file mode 100644
index 0000000..aca5c0b
--- /dev/null
+++ b/car-media-common/res/drawable/circular_progress_bar.xml
@@ -0,0 +1,41 @@
+<?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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:id="@android:id/background">
+        <shape android:shape="ring"
+               android:thickness="@dimen/minimized_progress_bar_track_thickness"
+               android:useLevel="false"/>
+    </item>
+
+    <item android:id="@android:id/progress">
+        <rotate android:fromDegrees="270"
+                android:toDegrees="270">
+            <shape android:shape="ring"
+                   android:thickness="@dimen/minimized_progress_bar_track_thickness"
+                   android:useLevel="true">
+                <gradient
+                    android:angle="0"
+                    android:endColor="@color/minimized_progress_bar_highlight"
+                    android:startColor="@color/minimized_progress_bar_highlight"
+                    android:type="sweep"
+                    android:useLevel="false"/>
+            </shape>
+        </rotate>
+    </item>
+
+</layer-list>
diff --git a/car-media-common/res/drawable/ic_app_switch.xml b/car-media-common/res/drawable/ic_app_switch.xml
new file mode 100644
index 0000000..ff485cf
--- /dev/null
+++ b/car-media-common/res/drawable/ic_app_switch.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z"/>
+</vector>
diff --git a/car-media-common/res/drawable/ic_media_select_arrow_drop_up.xml b/car-media-common/res/drawable/ic_media_select_arrow_drop_up.xml
deleted file mode 100644
index 1ba2974..0000000
--- a/car-media-common/res/drawable/ic_media_select_arrow_drop_up.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright 2018, The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M7,14l5,-5 5,5z"/>
-</vector>
diff --git a/car-media-common/res/drawable/ic_placeholder_0.xml b/car-media-common/res/drawable/ic_placeholder_0.xml
index 9d6f701..8c221f5 100644
--- a/car-media-common/res/drawable/ic_placeholder_0.xml
+++ b/car-media-common/res/drawable/ic_placeholder_0.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:viewportHeight="103"
     android:viewportWidth="103">
-    <path android:fillColor="#669DF6" android:pathData="M0 0h103v103H0z"/>
+    <path android:fillColor="@color/placeholder_color_0" android:pathData="M0 0h103v103H0z"/>
     <path android:fillColor="#000000" android:fillType="evenOdd"
         android:pathData="M52,34.5L52,53.842C50.918,53.218 49.672,52.833 48.333,52.833C44.282,52.833 41,56.115 41,60.167C41,64.218 44.282,67.5 48.333,67.5C52.385,67.5 55.667,64.218 55.667,60.167L55.667,41.833L63,41.833L63,34.5L52,34.5Z"
         android:strokeColor="#000000" android:strokeWidth="1"/>
diff --git a/car-media-common/res/drawable/ic_placeholder_1.xml b/car-media-common/res/drawable/ic_placeholder_1.xml
index c5fa23d..9fff5be 100644
--- a/car-media-common/res/drawable/ic_placeholder_1.xml
+++ b/car-media-common/res/drawable/ic_placeholder_1.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:viewportHeight="103"
     android:viewportWidth="103">
-    <path android:fillColor="#667EF6" android:pathData="M0 0h103v103H0z"/>
+    <path android:fillColor="@color/placeholder_color_1" android:pathData="M0 0h103v103H0z"/>
     <path android:fillColor="#000000" android:fillType="evenOdd"
         android:pathData="M52,34.5L52,53.842C50.918,53.218 49.672,52.833 48.333,52.833C44.282,52.833 41,56.115 41,60.167C41,64.218 44.282,67.5 48.333,67.5C52.385,67.5 55.667,64.218 55.667,60.167L55.667,41.833L63,41.833L63,34.5L52,34.5Z"
         android:strokeColor="#000000" android:strokeWidth="1"/>
diff --git a/car-media-common/res/drawable/ic_placeholder_2.xml b/car-media-common/res/drawable/ic_placeholder_2.xml
index 73f3283..1fbf27e 100644
--- a/car-media-common/res/drawable/ic_placeholder_2.xml
+++ b/car-media-common/res/drawable/ic_placeholder_2.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:viewportHeight="103"
     android:viewportWidth="103">
-    <path android:fillColor="#CA71E6" android:pathData="M0 0h103v103H0z"/>
+    <path android:fillColor="@color/placeholder_color_2" android:pathData="M0 0h103v103H0z"/>
     <path android:fillColor="#000000" android:fillType="evenOdd"
         android:pathData="M52,34.5L52,53.842C50.918,53.218 49.672,52.833 48.333,52.833C44.282,52.833 41,56.115 41,60.167C41,64.218 44.282,67.5 48.333,67.5C52.385,67.5 55.667,64.218 55.667,60.167L55.667,41.833L63,41.833L63,34.5L52,34.5Z"
         android:strokeColor="#000000" android:strokeWidth="1"/>
diff --git a/car-media-common/res/drawable/ic_placeholder_3.xml b/car-media-common/res/drawable/ic_placeholder_3.xml
index d0de3ed..b2b9b85 100644
--- a/car-media-common/res/drawable/ic_placeholder_3.xml
+++ b/car-media-common/res/drawable/ic_placeholder_3.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:viewportHeight="103"
     android:viewportWidth="103">
-    <path android:fillColor="#85B95B" android:pathData="M0 0h103v103H0z"/>
+    <path android:fillColor="@color/placeholder_color_3" android:pathData="M0 0h103v103H0z"/>
     <path android:fillColor="#000000" android:fillType="evenOdd"
         android:pathData="M52,34.5L52,53.842C50.918,53.218 49.672,52.833 48.333,52.833C44.282,52.833 41,56.115 41,60.167C41,64.218 44.282,67.5 48.333,67.5C52.385,67.5 55.667,64.218 55.667,60.167L55.667,41.833L63,41.833L63,34.5L52,34.5Z"
         android:strokeColor="#000000" android:strokeWidth="1"/>
diff --git a/car-media-common/res/drawable/ic_placeholder_4.xml b/car-media-common/res/drawable/ic_placeholder_4.xml
index 255023c..fb48e64 100644
--- a/car-media-common/res/drawable/ic_placeholder_4.xml
+++ b/car-media-common/res/drawable/ic_placeholder_4.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:viewportHeight="103"
     android:viewportWidth="103">
-    <path android:fillColor="#5BB974" android:pathData="M0 0h103v103H0z"/>
+    <path android:fillColor="@color/placeholder_color_4" android:pathData="M0 0h103v103H0z"/>
     <path android:fillColor="#000000" android:fillType="evenOdd"
         android:pathData="M52,34.5L52,53.842C50.918,53.218 49.672,52.833 48.333,52.833C44.282,52.833 41,56.115 41,60.167C41,64.218 44.282,67.5 48.333,67.5C52.385,67.5 55.667,64.218 55.667,60.167L55.667,41.833L63,41.833L63,34.5L52,34.5Z"
         android:strokeColor="#000000" android:strokeWidth="1"/>
diff --git a/car-media-common/res/drawable/ic_placeholder_5.xml b/car-media-common/res/drawable/ic_placeholder_5.xml
index a253646..030cefa 100644
--- a/car-media-common/res/drawable/ic_placeholder_5.xml
+++ b/car-media-common/res/drawable/ic_placeholder_5.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:viewportHeight="103"
     android:viewportWidth="103">
-    <path android:fillColor="#EE925C" android:pathData="M0 0h103v103H0z"/>
+    <path android:fillColor="@color/placeholder_color_5" android:pathData="M0 0h103v103H0z"/>
     <path android:fillColor="#000000" android:fillType="evenOdd"
         android:pathData="M52,34.5L52,53.842C50.918,53.218 49.672,52.833 48.333,52.833C44.282,52.833 41,56.115 41,60.167C41,64.218 44.282,67.5 48.333,67.5C52.385,67.5 55.667,64.218 55.667,60.167L55.667,41.833L63,41.833L63,34.5L52,34.5Z"
         android:strokeColor="#000000" android:strokeWidth="1"/>
diff --git a/car-media-common/res/drawable/ic_placeholder_6.xml b/car-media-common/res/drawable/ic_placeholder_6.xml
index 8b7d3a7..56c6498 100644
--- a/car-media-common/res/drawable/ic_placeholder_6.xml
+++ b/car-media-common/res/drawable/ic_placeholder_6.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:viewportHeight="103"
     android:viewportWidth="103">
-    <path android:fillColor="#EE675C" android:pathData="M0 0h103v103H0z"/>
+    <path android:fillColor="@color/placeholder_color_6" android:pathData="M0 0h103v103H0z"/>
     <path android:fillColor="#000000" android:fillType="evenOdd"
         android:pathData="M52,34.5L52,53.842C50.918,53.218 49.672,52.833 48.333,52.833C44.282,52.833 41,56.115 41,60.167C41,64.218 44.282,67.5 48.333,67.5C52.385,67.5 55.667,64.218 55.667,60.167L55.667,41.833L63,41.833L63,34.5L52,34.5Z"
         android:strokeColor="#000000" android:strokeWidth="1"/>
diff --git a/car-media-common/res/drawable/ic_placeholder_7.xml b/car-media-common/res/drawable/ic_placeholder_7.xml
index 319fd19..9532487 100644
--- a/car-media-common/res/drawable/ic_placeholder_7.xml
+++ b/car-media-common/res/drawable/ic_placeholder_7.xml
@@ -19,7 +19,7 @@
     android:width="48dp"
     android:viewportHeight="103"
     android:viewportWidth="103">
-    <path android:fillColor="#FCC933" android:pathData="M0 0h103v103H0z"/>
+    <path android:fillColor="@color/placeholder_color_7" android:pathData="M0 0h103v103H0z"/>
     <path android:fillColor="#000000" android:fillType="evenOdd"
         android:pathData="M52,34.5L52,53.842C50.918,53.218 49.672,52.833 48.333,52.833C44.282,52.833 41,56.115 41,60.167C41,64.218 44.282,67.5 48.333,67.5C52.385,67.5 55.667,64.218 55.667,60.167L55.667,41.833L63,41.833L63,34.5L52,34.5Z"
         android:strokeColor="#000000" android:strokeWidth="1"/>
diff --git a/car-media-common/res/drawable/ic_tracklist.xml b/car-media-common/res/drawable/ic_tracklist.xml
index 97f182f..fadaa7e 100644
--- a/car-media-common/res/drawable/ic_tracklist.xml
+++ b/car-media-common/res/drawable/ic_tracklist.xml
@@ -15,8 +15,8 @@
   limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="@dimen/appbar_view_icon_size"
-        android:height="@dimen/appbar_view_icon_size"
+        android:width="48dp"
+        android:height="48dp"
         android:viewportWidth="48"
         android:viewportHeight="48"
         android:tint="@color/selectable_button_tint_selector">
diff --git a/car-media-common/res/drawable/minimized_progress_bar_background.xml b/car-media-common/res/drawable/minimized_progress_bar_background.xml
index c1f45d3..c339b1e 100644
--- a/car-media-common/res/drawable/minimized_progress_bar_background.xml
+++ b/car-media-common/res/drawable/minimized_progress_bar_background.xml
@@ -18,14 +18,14 @@
 
     <item android:id="@android:id/background">
         <shape android:shape="line">
-            <stroke android:width="@dimen/minimized_progress_bar_track_height"/>
+            <stroke android:width="@dimen/minimized_progress_bar_track_thickness"/>
         </shape>
     </item>
 
     <item android:id="@android:id/progress">
         <clip>
             <shape android:shape="line">
-                <stroke android:width="@dimen/minimized_progress_bar_track_height"/>
+                <stroke android:width="@dimen/minimized_progress_bar_track_thickness"/>
             </shape>
         </clip>
     </item>
diff --git a/car-media-common/res/layout/app_selection_item.xml b/car-media-common/res/layout/app_selection_item.xml
deleted file mode 100644
index 92e177a..0000000
--- a/car-media-common/res/layout/app_selection_item.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~
-  ~ Copyright (C) 2018 Google Inc.
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT 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:id="@+id/app_item"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:layout_marginStart="@dimen/app_selection_item_margin_h"
-    android:layout_marginEnd="@dimen/app_selection_item_margin_h"
-    android:layout_marginBottom="@dimen/app_selection_item_margin_bottom"
-    android:orientation="vertical"
-    android:background="@drawable/car_card_ripple_background"
-    android:padding="@dimen/app_selection_item_padding"
-    android:gravity="center">
-
-    <ImageView
-        android:id="@+id/app_icon"
-        android:layout_width="@dimen/app_selection_item_app_icon_size"
-        android:layout_height="@dimen/app_selection_item_app_icon_size"
-        android:layout_marginBottom="@dimen/app_selection_item_app_icon_margin_bottom"
-        android:layout_gravity="center_horizontal" />
-
-    <TextView
-        android:id="@+id/app_name"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textAppearance="?attr/textAppearanceGridItem"
-        android:gravity="center"
-        android:singleLine="true"/>
-</LinearLayout>
diff --git a/car-media-common/res/layout/app_switch_widget.xml b/car-media-common/res/layout/app_switch_widget.xml
deleted file mode 100644
index 10629c6..0000000
--- a/car-media-common/res/layout/app_switch_widget.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright 2018, The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-      http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
--->
-<merge
-    xmlns:android="http://schemas.android.com/apk/res/android">
-    <ImageView
-        android:id="@+id/app_switch_icon"
-        android:layout_width="@dimen/app_switch_widget_icon_size"
-        android:layout_height="@dimen/app_switch_widget_icon_size"
-        android:tint="@color/app_switch_widget_icon_tint"/>
-
-    <ImageView
-        android:id="@+id/app_icon"
-        android:layout_width="@dimen/app_switch_widget_icon_size"
-        android:layout_height="@dimen/app_switch_widget_icon_size"/>
-</merge>
diff --git a/car-media-common/res/layout/fragment_app_selection.xml b/car-media-common/res/layout/fragment_app_selection.xml
deleted file mode 100644
index 05f1dcd..0000000
--- a/car-media-common/res/layout/fragment_app_selection.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~
-  ~ Copyright (C) 2018 Google Inc.
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT 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:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:gravity="center">
-
-<androidx.constraintlayout.widget.ConstraintLayout
-    android:id="@+id/actual_content"
-    android:background="@color/app_selection_background"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:layout_margin="80dp">
-
-    <androidx.constraintlayout.widget.Guideline
-        android:id="@+id/app_bar_bottom"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal"
-        app:layout_constraintGuide_begin="@dimen/appbar_first_row_height" />
-
-    <com.android.car.media.common.MediaAppSelectorWidget
-        android:id="@+id/app_switch_container"
-        android:layout_width="@dimen/app_switch_widget_width"
-        android:layout_height="@dimen/appbar_view_icon_touch_target_size"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="@+id/app_bar_bottom"
-        app:layout_constraintRight_toRightOf="parent"
-        android:padding="@dimen/app_switch_widget_icon_padding"
-        android:orientation="horizontal"
-        android:background="@drawable/appbar_view_icon_background"
-        android:gravity="center" />
-
-    <com.android.car.apps.common.widget.PagedRecyclerView
-        android:id="@+id/apps_grid"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/appbar_first_row_height"
-        android:clickable="true"
-        android:focusable="true"
-        />
-
-</androidx.constraintlayout.widget.ConstraintLayout>
-</FrameLayout>
diff --git a/car-media-common/res/layout/minimized_play_pause_stop_button_layout.xml b/car-media-common/res/layout/minimized_play_pause_stop_button_layout.xml
deleted file mode 100644
index c5d298b..0000000
--- a/car-media-common/res/layout/minimized_play_pause_stop_button_layout.xml
+++ /dev/null
@@ -1,25 +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.
-  -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
diff --git a/car-media-common/res/layout/minimized_playback_control_bar.xml b/car-media-common/res/layout/minimized_playback_control_bar.xml
index 4934b87..0b14759 100644
--- a/car-media-common/res/layout/minimized_playback_control_bar.xml
+++ b/car-media-common/res/layout/minimized_playback_control_bar.xml
@@ -23,7 +23,7 @@
     android:background="@drawable/minimized_control_bar_background">
 
     <ProgressBar
-        android:id="@+id/progress_bar"
+        android:id="@+id/linear_progress_bar"
         style="?android:attr/progressBarStyleHorizontal"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/car-media-common/res/layout/play_pause_stop_button_layout.xml b/car-media-common/res/layout/play_pause_stop_button_layout.xml
index f191dae..f7700fe 100644
--- a/car-media-common/res/layout/play_pause_stop_button_layout.xml
+++ b/car-media-common/res/layout/play_pause_stop_button_layout.xml
@@ -14,22 +14,29 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-    <!-- The invisible foreground ripple stops Android O from drawing an ugly square over the play
-    button -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/play_pause_container"
+    android:focusable="false"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+    <!-- The invisible foreground ripple stops Android O from drawing an ugly square over the play button -->
     <com.android.car.media.common.PlayPauseStopImageView
         android:id="@+id/play_pause_stop"
         style="@style/Widget.ActionButton"
         android:foreground="@drawable/fab_empty_foreground"
         android:src="@drawable/ic_play_pause_stop_animated"/>
     <ProgressBar
-        android:id="@+id/spinner"
+        android:id="@+id/circular_progress_bar"
         android:layout_width="@dimen/fab_spinner_size"
         android:layout_height="@dimen/fab_spinner_size"
         android:layout_gravity="center"
         android:padding="9dp"
         android:indeterminateDrawable="@drawable/music_buffering"
         android:indeterminateTint="@color/fab_spinner_indeterminate_color"
+        android:progressDrawable="@drawable/circular_progress_bar"
+        android:progressTint="@color/minimized_progress_bar_highlight"
+        android:progressBackgroundTint="@color/minimized_progress_bar_background"
         android:focusable="false"
-        android:visibility="invisible" />
-</merge>
+        android:indeterminateOnly="false"/>
+</FrameLayout>
diff --git a/car-media-common/res/layout/playback_fragment.xml b/car-media-common/res/layout/playback_fragment.xml
index 8123ca1..6d751cc 100644
--- a/car-media-common/res/layout/playback_fragment.xml
+++ b/car-media-common/res/layout/playback_fragment.xml
@@ -41,6 +41,18 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
+        <ImageView
+            android:id="@+id/app_icon"
+            android:layout_width="@dimen/app_selector_icon_size"
+            android:layout_height="@dimen/app_selector_icon_size"
+            android:layout_gravity="center"
+            android:layout_marginLeft="@dimen/playback_fragment_text_margin_x"
+            android:background="?android:attr/selectableItemBackground"
+            android:src="@drawable/ic_app_switch"
+            app:layout_constraintTop_toTopOf="@+id/app_name"
+            app:layout_constraintBottom_toBottomOf="@+id/app_name"
+            app:layout_constraintLeft_toLeftOf="parent"/>
+
         <TextView
             android:id="@+id/app_name"
             android:layout_width="0dp"
@@ -51,23 +63,10 @@
             android:textAppearance="?android:attr/textAppearanceMedium"
             android:singleLine="true"
             android:includeFontPadding="false"
-            app:layout_constraintLeft_toLeftOf="parent"
-            app:layout_constraintRight_toLeftOf="@+id/app_switch_container"
+            app:layout_constraintLeft_toRightOf="@+id/app_icon"
+            app:layout_constraintRight_toLeftOf="@+id/app_selector_container"
             app:layout_constraintTop_toTopOf="parent"/>
 
-        <com.android.car.media.common.MediaAppSelectorWidget
-            android:id="@+id/app_switch_container"
-            android:layout_width="@dimen/app_switch_widget_width"
-            android:layout_height="@dimen/appbar_view_icon_touch_target_size"
-            app:layout_constraintRight_toRightOf="parent"
-            app:layout_constraintTop_toTopOf="@+id/app_name"
-            app:layout_constraintBottom_toBottomOf="@+id/app_name"
-            app:fullScreenDialog="false"
-            android:padding="@dimen/app_switch_widget_icon_padding"
-            android:orientation="horizontal"
-            android:background="@drawable/appbar_view_icon_background"
-            android:gravity="center" />
-
         <TextView
             android:id="@+id/title"
             style="@style/PlaybackTitleStyle"
@@ -89,9 +88,29 @@
             android:layout_marginLeft="@dimen/playback_fragment_text_margin_x"
             android:layout_marginRight="@dimen/playback_fragment_text_margin_x"
             app:layout_constraintLeft_toLeftOf="parent"
-            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintRight_toLeftOf="@+id/app_selector_container"
             app:layout_constraintTop_toBottomOf="@+id/title"/>
 
+        <FrameLayout
+            android:id="@+id/app_selector_container"
+            xmlns:android="http://schemas.android.com/apk/res/android"
+            android:layout_width="@dimen/app_selector_icon_touch_target"
+            android:layout_height="@dimen/app_selector_icon_touch_target"
+            android:background="?android:attr/selectableItemBackground"
+            android:layout_marginRight="@dimen/app_selector_margin_x"
+            app:layout_constraintTop_toTopOf="@+id/app_name"
+            app:layout_constraintBottom_toBottomOf="@+id/app_name"
+            app:layout_constraintRight_toRightOf="parent">
+
+            <ImageView
+                android:id="@+id/app_selector"
+                android:layout_width="@dimen/app_selector_icon_size"
+                android:layout_height="@dimen/app_selector_icon_size"
+                android:layout_gravity="center"
+                android:src="@drawable/ic_app_switch"
+                android:tint="@color/icon_tint" />
+        </FrameLayout>
+
         <com.android.car.media.common.PlaybackControlsActionBar
             android:id="@+id/playback_controls"
             android:layout_width="0dp"
diff --git a/car-media-common/res/values-h668dp/dimens.xml b/car-media-common/res/values-h600dp/dimens.xml
similarity index 61%
copy from car-media-common/res/values-h668dp/dimens.xml
copy to car-media-common/res/values-h600dp/dimens.xml
index 3ca1445..6b7980a 100644
--- a/car-media-common/res/values-h668dp/dimens.xml
+++ b/car-media-common/res/values-h600dp/dimens.xml
@@ -15,7 +15,12 @@
   limitations under the License.
 -->
 <resources>
-    <!-- App bar -->
-    <!-- The height of app bar when it expends to 2 rows. Equals 2 * @dimen/appbar_first_row_height. -->
     <dimen name="appbar_2_rows_height">192dp</dimen>
+    <dimen name="appbar_view_icon_touch_target_size">96dp</dimen>
+    <dimen name="appbar_view_icon_padding">26dp</dimen>
+    <dimen name="appbar_view_icon_background_radius">48dp</dimen>
+    <!-- negative margins to overlap icons on search bar -->
+    <dimen name="appbar_view_search_app_icon_margin">-76dp</dimen>
+    <dimen name="appbar_view_search_close_icon_margin">-96dp</dimen>
+    <dimen name="appbar_view_search_bar_end_margin">26dp</dimen>
 </resources>
diff --git a/car-media-common/res/values-port/dimens.xml b/car-media-common/res/values-port/dimens.xml
index 4207cf2..721ed93 100644
--- a/car-media-common/res/values-port/dimens.xml
+++ b/car-media-common/res/values-port/dimens.xml
@@ -15,12 +15,6 @@
   limitations under the License.
 -->
 <resources>
-    <!-- Fab -->
-    <dimen name="fab_large_size">76dp</dimen>
-    <dimen name="fab_large_padding">20dp</dimen>
-    <dimen name="fab_spinner_size">80dp</dimen>
-
     <!-- App bar -->
     <dimen name="appbar_second_row_height">@*android:dimen/car_app_bar_height</dimen>
-
 </resources>
diff --git a/car-media-common/res/values/arrays.xml b/car-media-common/res/values/arrays.xml
index a83ee0c..55e08b9 100644
--- a/car-media-common/res/values/arrays.xml
+++ b/car-media-common/res/values/arrays.xml
@@ -16,6 +16,11 @@
   -->
 
 <resources>
+    <!-- These are drawables to be used as placeholder album art. Album art
+        can be displayed in 2 modes, foreground or background. Foreground art
+        is displayed prominently (such as with other song metadata), whereas
+        background art is only used for backgrounds, and is typically
+        partially blurred/hidden. -->
     <array name="placeholder_images">
         <item>@drawable/ic_placeholder_0</item>
         <item>@drawable/ic_placeholder_1</item>
@@ -26,4 +31,23 @@
         <item>@drawable/ic_placeholder_6</item>
         <item>@drawable/ic_placeholder_7</item>
     </array>
+    <!-- This array is for the backgrounds of the placeholder images above -->
+    <array name="placeholder_backgrounds">
+        <item>@drawable/placeholder_color_drawable_0</item>
+        <item>@drawable/placeholder_color_drawable_1</item>
+        <item>@drawable/placeholder_color_drawable_2</item>
+        <item>@drawable/placeholder_color_drawable_3</item>
+        <item>@drawable/placeholder_color_drawable_4</item>
+        <item>@drawable/placeholder_color_drawable_5</item>
+        <item>@drawable/placeholder_color_drawable_6</item>
+        <item>@drawable/placeholder_color_drawable_7</item>
+    </array>
+
+
+    <!-- These media sources are displayed by the AppSelectionFragment in the order defined here.
+        Other sources follow in alphabetical order. -->
+    <string-array name="preferred_media_sources" translatable="false">
+        <item>com.android.car.radio/.service.RadioAppService</item>
+    </string-array>
+
 </resources>
\ No newline at end of file
diff --git a/car-media-common/res/values/attrs.xml b/car-media-common/res/values/attrs.xml
index 6530810..75ff716 100644
--- a/car-media-common/res/values/attrs.xml
+++ b/car-media-common/res/values/attrs.xml
@@ -38,16 +38,4 @@
             <enum name="right" value="2"/>
         </attr>
     </declare-styleable>
-
-    <!-- Attributes for the MediaAppSelectorWidget. -->
-    <declare-styleable name="MediaAppSelectorWidget">
-        <!-- When false, the widget only displays the application's icon and doesn't react to taps.
-            Default is true. -->
-        <attr name="switchingEnabled" format="boolean"/>
-
-        <!-- Whether the AppSelectionFragment opened by pressing the widget should be full screen.
-            -->
-        <attr name="fullScreenDialog" format="boolean"/>
-
-    </declare-styleable>
 </resources>
diff --git a/car-media-common/res/values/bools.xml b/car-media-common/res/values/bools.xml
index f1f45fe..85fce20 100644
--- a/car-media-common/res/values/bools.xml
+++ b/car-media-common/res/values/bools.xml
@@ -17,11 +17,9 @@
 <resources>
     <bool name="use_media_source_color_for_fab_spinner">true</bool>
 
-    <!-- This value must remain false for production builds. It is intended to be overlaid in
-        the simulator, so media app developers can notice quickly that they are sending invalid
-        art (see MediaItemMetadata and MediaButtonController).
-        The same effect can be achieved with: adb shell setprop log.tag.MediaItemFlagInvalidArt 1
-    -->
-    <bool name="flag_invalid_media_art">false</bool>
+    <!-- Whether to show a linear progress bar in minimized control bar or not. -->
+    <bool name="show_linear_progress_bar">true</bool>
+    <!-- Whether to show a circular progress bar in control bar and minimized control bar or not. -->
+    <bool name="show_circular_progress_bar">false</bool>
 
 </resources>
diff --git a/car-media-common/res/values/colors.xml b/car-media-common/res/values/colors.xml
index d55ea69..51927a7 100644
--- a/car-media-common/res/values/colors.xml
+++ b/car-media-common/res/values/colors.xml
@@ -18,12 +18,6 @@
     <!-- Album art scrim -->
     <color name="album_art_scrim">#000000</color>
 
-    <!-- app_switch_widget.xml -->
-    <color name="app_switch_widget_icon_tint">@color/primary_app_icon_color</color>
-
-    <!-- App selection background -->
-    <color name="app_selection_border">#40000000</color>
-    <color name="app_selection_background">#ff000000</color>
     <!-- App icons selection background -->
     <color name="appbar_view_icon_background_color">#66ffffff</color>
 
@@ -38,10 +32,24 @@
     <color name="media_source_default_color">@android:color/background_dark</color>
 
     <!-- Color used on the minimized progress bar background -->
-    <color name="minimized_progress_bar_background">#00000000</color>
+    <color name="minimized_progress_bar_background">#464A4D</color>
     <!-- Color used on the minimized progress bar -->
     <color name="minimized_progress_bar_highlight">@color/media_source_default_color</color>
 
     <!-- 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>
+    <color name="placeholder_color_2">#CA71E6</color>
+    <color name="placeholder_color_3">#85B95B</color>
+    <color name="placeholder_color_4">#5BB974</color>
+    <color name="placeholder_color_5">#EE925C</color>
+    <color name="placeholder_color_6">#EE675C</color>
+    <color name="placeholder_color_7">#FCC933</color>
+
 </resources>
diff --git a/car-media-common/res/values/config.xml b/car-media-common/res/values/config.xml
new file mode 100644
index 0000000..34c1eb6
--- /dev/null
+++ b/car-media-common/res/values/config.xml
@@ -0,0 +1,37 @@
+<?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.
+  -->
+
+<resources>
+    <!-- Intent used to launch the app selector as popup -->
+    <string name="launcher_popup_intent" translatable="false">
+        intent:#Intent;component=com.android.car.carlauncher/.AppGridActivity;launchFlags=0x24000000;S.com.android.car.carlauncher.mode=MEDIA_POPUP;end
+    </string>
+
+    <!-- Intent used to launch the app selector -->
+    <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>
+
+    <!-- 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/dimens.xml b/car-media-common/res/values/dimens.xml
index ba435e3..eedc8a6 100644
--- a/car-media-common/res/values/dimens.xml
+++ b/car-media-common/res/values/dimens.xml
@@ -19,22 +19,8 @@
     <item name="album_art_scrim_alpha" format="float" type="dimen">0.75</item>
 
     <!-- Fab -->
-    <dimen name="fab_elevation">8dp</dimen>
-    <dimen name="fab_large_size">96dp</dimen>
-    <dimen name="fab_large_padding">27dp</dimen>
     <dimen name="fab_spinner_size">128dp</dimen>
 
-    <!-- App bar -->
-    <dimen name="appbar_first_row_height">@*android:dimen/car_app_bar_height</dimen>
-    <!-- By default app bar has only 1 row -->
-    <dimen name="appbar_second_row_height">0dp</dimen>
-    <!-- The height of app bar when it expends to 2 rows. Equals 2 * @dimen/appbar_first_row_height. -->
-    <dimen name="appbar_2_rows_height">160dp</dimen>
-    <dimen name="appbar_view_icon_touch_target_size">96dp</dimen>
-    <dimen name="appbar_view_icon_size">@*android:dimen/car_primary_icon_size</dimen>
-    <!-- Padding of primary icon, equals (@dimen/appbar_view_icon_touch_target_size - @dimen/appbar_view_icon_size) / 2. -->
-    <dimen name="appbar_view_icon_padding">26dp</dimen>
-
     <!-- playback_fragment.xml -->
     <dimen name="playback_fragment_text_margin_top">@*android:dimen/car_padding_4</dimen>
     <dimen name="playback_fragment_text_margin_x">@*android:dimen/car_padding_4</dimen>
@@ -42,27 +28,16 @@
     <dimen name="playback_fragment_app_icon_margin_right">@*android:dimen/car_padding_4</dimen>
     <dimen name="playback_fragment_controls_margin_bottom">@*android:dimen/car_padding_4</dimen>
 
-    <!-- app_switch_widget.xml -->
-    <dimen name="app_switch_widget_width">@*android:dimen/car_margin</dimen>
-    <dimen name="app_switch_widget_icon_size">@*android:dimen/car_primary_icon_size</dimen>
-    <dimen name="app_switch_widget_icon_padding">@*android:dimen/car_padding_1</dimen>
-
-    <!-- app_selection_item.xml -->
-    <dimen name="app_selection_item_margin_h">@*android:dimen/car_padding_2</dimen>
-    <dimen name="app_selection_item_margin_bottom">@*android:dimen/car_padding_5</dimen>
-    <dimen name="app_selection_item_padding">@*android:dimen/car_padding_1</dimen>
-    <dimen name="app_selection_item_app_icon_size">@*android:dimen/car_touch_target_size</dimen>
-    <dimen name="app_selection_item_app_icon_margin_bottom">@*android:dimen/car_padding_4</dimen>
-
     <!-- Drawable appbar_view_icon_background.xml -->
     <dimen name="appbar_view_icon_background_corner_radius">@*android:dimen/car_radius_2</dimen>
-
-    <!-- fragment_app_selection.xml -->
-    <dimen name="fragment_app_selection_list_top_offset">@*android:dimen/car_padding_5</dimen>
+    <dimen name="appbar_view_icon_touch_target_size">76dp</dimen>
+    <dimen name="appbar_view_icon_background_radius">38dp</dimen>
 
     <!-- Minimized Progress Bar -->
-    <dimen name="minimized_progress_bar_track_height">4dp</dimen>
+    <dimen name="minimized_progress_bar_track_thickness">4dp</dimen>
 
-    <!-- appbar_view_icon_background.xml -->
-    <dimen name="appbar_view_icon_background_radius">48dp</dimen>
+    <!-- App selector -->
+    <dimen name="app_selector_icon_size">@*android:dimen/car_primary_icon_size</dimen>
+    <dimen name="app_selector_icon_touch_target">@*android:dimen/car_touch_target_size</dimen>
+    <dimen name="app_selector_margin_x">@*android:dimen/car_padding_2</dimen>
 </resources>
diff --git a/car-media-common/res/values/drawables.xml b/car-media-common/res/values/drawables.xml
new file mode 100644
index 0000000..c073df9
--- /dev/null
+++ b/car-media-common/res/values/drawables.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <drawable name="placeholder_color_drawable_0">@color/placeholder_color_0</drawable>
+  <drawable name="placeholder_color_drawable_1">@color/placeholder_color_1</drawable>
+  <drawable name="placeholder_color_drawable_2">@color/placeholder_color_2</drawable>
+  <drawable name="placeholder_color_drawable_3">@color/placeholder_color_3</drawable>
+  <drawable name="placeholder_color_drawable_4">@color/placeholder_color_4</drawable>
+  <drawable name="placeholder_color_drawable_5">@color/placeholder_color_5</drawable>
+  <drawable name="placeholder_color_drawable_6">@color/placeholder_color_6</drawable>
+  <drawable name="placeholder_color_drawable_7">@color/placeholder_color_7</drawable>
+</resources>
\ No newline at end of file
diff --git a/car-media-common/res/values/integers.xml b/car-media-common/res/values/integers.xml
index 3e830cd..2c6177d 100644
--- a/car-media-common/res/values/integers.xml
+++ b/car-media-common/res/values/integers.xml
@@ -26,4 +26,14 @@
 
     <!-- Number of columns in app selector -->
     <integer name="num_app_selector_columns">3</integer>
+
+    <!-- Defines the maximum square into which requested media items bitmaps must fit (smaller
+        bitmaps are not modified, larger ones are resized to fit into the square, preserving their
+        aspect ratio). A 256x256 bitmap takes 250 KB or memory, and a 512x512 takes 1 MB so this
+        number should be set conservatively while making sure the displayed bitmaps look good.
+        This value is also passed to media applications so they can pre-fetch their artwork at an
+        optimal resolution.
+    -->
+    <integer name="media_items_bitmap_max_size_px">256</integer>
+
 </resources>
diff --git a/car-media-common/res/values/styles.xml b/car-media-common/res/values/styles.xml
index d4795b3..5d00241 100644
--- a/car-media-common/res/values/styles.xml
+++ b/car-media-common/res/values/styles.xml
@@ -15,30 +15,16 @@
   limitations under the License.
 -->
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <style name="MediaAppSelectorStyle" parent="android:Theme.DeviceDefault.NoActionBar">
-        <item name="android:windowIsTranslucent">true</item>
-        <item name="android:windowBackground">@color/app_selection_border</item>
-        <item name="android:windowNoTitle">true</item>
-        <item name="android:windowBackground">@*android:color/car_card</item>
-        <item name="android:fragmentOpenEnterAnimation">@anim/media_app_selector_fade_in</item>
-    </style>
-
-    <style
-        name="media_app_selector_animation_fade" >
-        <item name="android:windowEnterAnimation">@anim/media_app_selector_fade_in</item>
-        <item name="android:windowExitAnimation">@anim/media_app_selector_fade_out</item>
-    </style>
-
     <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/AppSelectionFragment.java b/car-media-common/src/com/android/car/media/common/AppSelectionFragment.java
deleted file mode 100644
index 088aeb6..0000000
--- a/car-media-common/src/com/android/car/media/common/AppSelectionFragment.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.car.media.common;
-
-import android.annotation.NonNull;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.GridLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.car.media.common.source.MediaSource;
-import com.android.car.media.common.source.MediaSourceViewModel;
-import com.android.car.media.common.source.MediaSourcesLiveData;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A {@link Fragment} that implements the app selection UI. It is typically created by a
- * {@link MediaAppSelectorWidget} widget that shows the current media source (stored as
- * {@link #mSelectorWidget}. To ensure visual coherence, the UI of the fragment also shows a
- * {@link MediaAppSelectorWidget} widget, but this one is in "display only" mode as tapping it
- * closes the fragment rather than opening a new one.
- * Note: the fragment dismisses itself in {@link #onStop} as its less confusing to come back to the
- * media app than the fragment after using another facet.
- */
-public class AppSelectionFragment extends DialogFragment {
-
-    private static final String ORIGIN_SOURCE_PACKAGE_KEY = "origin_source_package_key";
-    private static final String FULL_SCREEN_KEY = "full_screen_key";
-
-    /** The widget that opened this fragment. */
-    private final MediaAppSelectorWidget mSelectorWidget;
-
-    private String mOriginSourcePackage;
-    private boolean mFullScreenDialog;
-
-    /** The widget contained by this fragment UI to display the current source. */
-    private MediaAppSelectorWidget mDisplayWidget;
-
-    /**
-     * Creates a new {@link AppSelectionFragment}.
-     * @param selectorWidget the widget that is opening this fragment
-     * @param originSourcePackage the media source package shown in the selectorWidget.
-     */
-    public static AppSelectionFragment create(MediaAppSelectorWidget selectorWidget,
-            String originSourcePackage, boolean fullScreenDialog) {
-        AppSelectionFragment result = new AppSelectionFragment(selectorWidget);
-        Bundle bundle = new Bundle(1);
-        bundle.putString(ORIGIN_SOURCE_PACKAGE_KEY, originSourcePackage);
-        bundle.putBoolean(FULL_SCREEN_KEY, fullScreenDialog);
-        result.setArguments(bundle);
-        return result;
-    }
-
-    public AppSelectionFragment() {
-        this(null);
-    }
-
-    private AppSelectionFragment(MediaAppSelectorWidget selectorWidget) {
-        mSelectorWidget = selectorWidget;
-    }
-
-
-
-    private class AppGridAdapter extends RecyclerView.Adapter<AppItemViewHolder> {
-        private List<MediaSource> mMediaSources;
-
-        /**
-         * Triggers a refresh of media sources
-         */
-        void updateSources(List<MediaSource> mediaSources) {
-            mMediaSources = new ArrayList<>(mediaSources);
-            notifyDataSetChanged();
-        }
-
-        @NonNull
-        @Override
-        public AppItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-            View view = LayoutInflater.from(parent.getContext())
-                    .inflate(R.layout.app_selection_item, parent, false);
-            return new AppItemViewHolder(view);
-
-        }
-
-        @Override
-        public void onBindViewHolder(@NonNull AppItemViewHolder vh, int position) {
-            vh.bind(mMediaSources.get(position));
-        }
-
-        @Override
-        public int getItemCount() {
-            return mMediaSources.size();
-        }
-    }
-
-    private class AppItemViewHolder extends RecyclerView.ViewHolder {
-        View mAppItem;
-        ImageView mAppIconView;
-        TextView mAppNameView;
-
-        AppItemViewHolder(View view) {
-            super(view);
-            mAppItem = view.findViewById(R.id.app_item);
-            mAppIconView = mAppItem.findViewById(R.id.app_icon);
-            mAppNameView = mAppItem.findViewById(R.id.app_name);
-        }
-
-        /**
-         * Binds a media source to a view
-         */
-        void bind(@NonNull MediaSource mediaSrc) {
-            mAppItem.setOnClickListener(
-                    v -> {
-                        MediaSourceViewModel model = MediaSourceViewModel.get(
-                                requireActivity().getApplication());
-                        model.setPrimaryMediaSource(mediaSrc);
-                        dismiss();
-                    });
-
-            mAppIconView.setImageDrawable(mediaSrc.getPackageIcon());
-            mAppNameView.setText(mediaSrc.getName());
-        }
-    }
-
-    /** Closes the selector (allowing state loss). */
-    @Override
-    public void dismiss() {
-        if (mSelectorWidget != null) {
-            mSelectorWidget.setIsOpen(false);
-        }
-        mDisplayWidget.setIsOpen(false);
-        // TODO(b/122324380) try using a shared element transition instead of this trick.
-        // The delay is needed to update the arrow in the MediaAppSelectorWidget of this fragment
-        // before the fragment fades away to reveal the underlying MediaAppSelectorWidget with an
-        // arrow pointing the other way. Otherwise we end up seeing the arrows pointing in opposite
-        // directions... Note that in the home screen the widgets are not overlapped.
-        mDisplayWidget.postDelayed(() -> dismissAllowingStateLoss(), 50);
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        if (getDialog() != null) {
-            getDialog().getWindow().setWindowAnimations(R.style.media_app_selector_animation_fade);
-        }
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        dismiss();
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setStyle(DialogFragment.STYLE_NORMAL, R.style.MediaAppSelectorStyle); // Full screen style.
-
-        Bundle args = getArguments();
-        if (args != null) {
-            mOriginSourcePackage = args.getString(ORIGIN_SOURCE_PACKAGE_KEY);
-            mFullScreenDialog = args.getBoolean(FULL_SCREEN_KEY, true);
-        }
-    }
-
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container,
-            Bundle savedInstanceState) {
-        View view = inflater.inflate(R.layout.fragment_app_selection, container, false);
-
-        if (mFullScreenDialog) {
-            ViewGroup contentView = view.findViewById(R.id.actual_content);
-            ViewGroup.MarginLayoutParams p =
-                    (ViewGroup.MarginLayoutParams) contentView.getLayoutParams();
-            p.setMargins(0, 0, 0, 0);
-        }
-
-        int columnNumber = getResources().getInteger(R.integer.num_app_selector_columns);
-        AppGridAdapter gridAdapter = new AppGridAdapter();
-        gridAdapter.updateSources(MediaSourcesLiveData.getInstance(getContext()).getList());
-        mDisplayWidget = view.findViewById(R.id.app_switch_container);
-        mDisplayWidget.setFragmentOwner(this);
-        mDisplayWidget.setFragmentActivity(getActivity());
-
-        RecyclerView gridView = view.findViewById(R.id.apps_grid);
-        GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), columnNumber);
-        gridView.setLayoutManager(gridLayoutManager);
-        gridView.setAdapter(gridAdapter);
-        return view;
-    }
-}
diff --git a/car-media-common/src/com/android/car/media/common/ControlBarHelper.java b/car-media-common/src/com/android/car/media/common/ControlBarHelper.java
new file mode 100644
index 0000000..0f011fd
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/ControlBarHelper.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.media.common;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.view.View;
+import android.widget.ProgressBar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.android.car.media.common.playback.PlaybackViewModel;
+
+/**
+ * Helper class for {@link PlaybackControlsActionBar} and {@link MinimizedPlaybackControlBar}.
+ */
+public class ControlBarHelper {
+
+    /**
+     * Initializes progress bar, i.e., sets progress tint and progress update listener.
+     */
+    public static void initProgressBar(@NonNull Context context, @NonNull LifecycleOwner owner,
+            @NonNull PlaybackViewModel model, @Nullable ProgressBar progressBar,
+            boolean showProgressBar) {
+        if (progressBar == null) {
+            return;
+        }
+        if (!showProgressBar) {
+            progressBar.setVisibility(View.GONE);
+            return;
+        }
+        boolean useMediaSourceColor =
+                context.getResources().getBoolean(
+                        R.bool.use_media_source_color_for_minimized_progress_bar);
+        int defaultColor = context.getResources().getColor(R.color.minimized_progress_bar_highlight,
+                null);
+        if (useMediaSourceColor) {
+            model.getMediaSourceColors().observe(owner,
+                    sourceColors -> {
+                        int color = sourceColors != null ? sourceColors.getAccentColor(
+                                defaultColor)
+                                : defaultColor;
+                        progressBar.setProgressTintList(ColorStateList.valueOf(color));
+                    });
+        } else {
+            progressBar.setProgressTintList(ColorStateList.valueOf(defaultColor));
+        }
+
+        model.getProgress().observe(owner,
+                progress -> {
+                    progressBar.setProgress((int) progress.getProgress());
+                    progressBar.setMax((int) progress.getMaxProgress());
+                });
+    }
+}
diff --git a/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java b/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java
index e827a0f..9012a3b 100644
--- a/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java
+++ b/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java
@@ -27,7 +27,7 @@
 /**
  * Abstract representation of a custom playback action. A custom playback action represents a
  * visual element that can be used to trigger playback actions not included in the standard
- * {@link PlaybackControls} class.
+ * control bar.
  * Custom actions for the current media source are exposed through
  * {@link PlaybackStateWrapper#getCustomActions}
  */
diff --git a/car-media-common/src/com/android/car/media/common/GridSpacingItemDecoration.java b/car-media-common/src/com/android/car/media/common/GridSpacingItemDecoration.java
index 3deea99..ed42a23 100644
--- a/car-media-common/src/com/android/car/media/common/GridSpacingItemDecoration.java
+++ b/car-media-common/src/com/android/car/media/common/GridSpacingItemDecoration.java
@@ -28,20 +28,14 @@
  */
 public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
     private final int mSpacing;
-    private final int mStartMargin;
-    private final int mEndMargin;
 
     /**
      * Creates a {@link GridSpacingItemDecoration}.
      *
-     * @param spacing     space to add between grid cells, both vertically and horizontally.
-     * @param startMargin margin on the start side of the grid.
-     * @param endMargin   margin on the end side of the grid
+     * @param spacing     space to add between grid cells horizontally.
      */
-    public GridSpacingItemDecoration(int spacing, int startMargin, int endMargin) {
+    public GridSpacingItemDecoration(int spacing) {
         this.mSpacing = spacing;
-        this.mStartMargin = startMargin;
-        this.mEndMargin = endMargin;
     }
 
     @Override
@@ -51,10 +45,7 @@
         GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
         int column = lp.getSpanIndex();
         int spanCount = layoutManager.getSpanCount();
-
-        outRect.left = mStartMargin + column * mSpacing / spanCount
-                - (column + 1) * mStartMargin / spanCount;
-        outRect.right = mSpacing - (column + 1) * mSpacing / spanCount
-                + (column + 1) * mEndMargin / spanCount;
+        outRect.left = column * mSpacing / spanCount;
+        outRect.right = mSpacing - (column + 1) * mSpacing / spanCount;
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/MediaAppSelectorWidget.java b/car-media-common/src/com/android/car/media/common/MediaAppSelectorWidget.java
deleted file mode 100644
index 81288bb..0000000
--- a/car-media-common/src/com/android/car/media/common/MediaAppSelectorWidget.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media.common;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-
-import androidx.fragment.app.FragmentActivity;
-
-import com.android.car.apps.common.util.ViewUtils;
-import com.android.car.media.common.source.MediaSourceViewModel;
-
-/**
- * Widget showing the icon of the currently selected media application as well as an arrow
- * indicating whether the selection UI is shown. The widget can be embedded both in an application
- * bar where tapping it opens an {@link AppSelectionFragment}, and also in the UI of the selection
- * fragment to provide visual continuity and a way to close the fragment without selecting an
- * application.
- * In order for the widget to connect to {@link MediaSourceViewModel} (so it can update its icon),
- * {@link #setFragmentActivity} must be called by the code that creates a view containing this
- * widget.
- */
-public class MediaAppSelectorWidget extends LinearLayout {
-
-    private final boolean mFullScreenDialog;
-    private final boolean mSwitchingEnabled;
-    private final ImageView mAppIcon;
-    private final ImageView mAppSwitchIcon;
-    private final Drawable mDefaultIcon;
-    private final Drawable mArrowDropDown;
-    private final Drawable mArrowDropUp;
-
-    private FragmentActivity mActivity;
-
-    /** The fragment that owns the widget (only set when in display only mode). */
-    @Nullable private AppSelectionFragment mFragmentOwner;
-    private String mDisplayedSourcePackage;
-    private boolean mFragmentIsOpen;
-
-    public MediaAppSelectorWidget(Context context) {
-        this(context, null);
-    }
-
-    public MediaAppSelectorWidget(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public MediaAppSelectorWidget(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public MediaAppSelectorWidget(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-
-        TypedArray a = context.obtainStyledAttributes(
-                attrs, R.styleable.MediaAppSelectorWidget, defStyleAttr, 0 /* defStyleRes */);
-        mFullScreenDialog = a.getBoolean(R.styleable.MediaAppSelectorWidget_fullScreenDialog, true);
-        mSwitchingEnabled = a.getBoolean(R.styleable.MediaAppSelectorWidget_switchingEnabled, true);
-        a.recycle();
-
-        mDefaultIcon = getResources().getDrawable(R.drawable.ic_music);
-        mArrowDropDown = getResources().getDrawable(R.drawable.ic_media_select_arrow_drop_down,
-                null);
-        mArrowDropUp = getResources().getDrawable(R.drawable.ic_media_select_arrow_drop_up, null);
-
-        LayoutInflater inflater = LayoutInflater.from(context);
-        inflater.inflate(R.layout.app_switch_widget, this, true);
-
-        mAppIcon = findViewById(R.id.app_icon);
-        mAppSwitchIcon = findViewById(R.id.app_switch_icon);
-
-        setFragmentOwner(null);
-        if (mSwitchingEnabled) {
-            setOnClickListener(view -> onAppSwitchClicked());
-        } else {
-            ViewUtils.setVisible(mAppSwitchIcon, false);
-        }
-    }
-
-    /** Calling this is required so the widget can show the icon of the primary media source. */
-    public void setFragmentActivity(FragmentActivity activity) {
-        mActivity = activity;
-        MediaSourceViewModel model = MediaSourceViewModel.get(activity.getApplication());
-        model.getPrimaryMediaSource().observe(activity, source -> {
-            if (source == null) {
-                setAppIcon(null);
-            } else {
-                mDisplayedSourcePackage = source.getPackageName();
-                setAppIcon(source.getRoundPackageIcon());
-            }
-        });
-    }
-
-    /** Opens the {@link AppSelectionFragment}. */
-    public void open() {
-        if (mSwitchingEnabled && !mFragmentIsOpen) {
-            onAppSwitchClicked();
-        }
-    }
-
-    /** Closes the {@link AppSelectionFragment}. */
-    public void close() {
-        if (mSwitchingEnabled && mFragmentIsOpen) {
-            onAppSwitchClicked();
-        }
-    }
-
-    /** Sets whether the widget is shown as part of an {@link AppSelectionFragment} UI. */
-    void setFragmentOwner(@Nullable AppSelectionFragment fragmentOwner) {
-        mFragmentOwner = fragmentOwner;
-        setIsOpen(mFragmentOwner != null);
-    }
-
-    void setIsOpen(boolean fragmentIsOpen) {
-        if (mSwitchingEnabled) {
-            mFragmentIsOpen = fragmentIsOpen;
-            mAppSwitchIcon.setImageDrawable(fragmentIsOpen ? mArrowDropUp : mArrowDropDown);
-        }
-    }
-
-    /**
-     * Updates the application icon to show next to the application switcher.
-     */
-    private void setAppIcon(Bitmap icon) {
-        if (icon != null) {
-            mAppIcon.setImageBitmap(icon);
-        } else {
-            mAppIcon.setImageDrawable(mDefaultIcon);
-        }
-    }
-
-    private void onAppSwitchClicked() {
-        if (mFragmentOwner != null) {
-            mFragmentOwner.dismiss();
-        } else {
-            setIsOpen(true);
-            AppSelectionFragment newFragment = AppSelectionFragment.create(this,
-                    mDisplayedSourcePackage, mFullScreenDialog);
-            newFragment.show(mActivity.getSupportFragmentManager(), null);
-        }
-
-    }
-}
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 1b4e8ae..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
@@ -35,6 +35,7 @@
 import androidx.lifecycle.LifecycleOwner;
 
 import com.android.car.apps.common.CarControlBar;
+import com.android.car.apps.common.CommonFlags;
 import com.android.car.apps.common.ControlBar;
 import com.android.car.media.common.playback.PlaybackViewModel;
 import com.android.car.media.common.source.MediaSourceColors;
@@ -52,18 +53,17 @@
 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;
     private View mPlayPauseStopImageContainer;
-    private ProgressBar mSpinner;
+    private ProgressBar mCircularProgressBar;
     private ImageButton mSkipPrevButton;
     private ImageButton mSkipNextButton;
     private ColorStateList mIconsColor;
     private boolean mSkipNextAdded;
     private boolean mSkipPrevAdded;
+    private boolean mShowCircularProgressBar;
 
     private PlaybackViewModel mModel;
     private PlaybackViewModel.PlaybackController mController;
@@ -80,11 +80,13 @@
         mPlayPauseStopImageContainer.setOnClickListener(this::onPlayPauseStopClicked);
         mPlayPauseStopImageView = mPlayPauseStopImageContainer.findViewById(R.id.play_pause_stop);
         mPlayPauseStopImageView.setVisibility(View.INVISIBLE);
-        mSpinner = mPlayPauseStopImageContainer.findViewById(R.id.spinner);
-        mSpinner.setVisibility(View.INVISIBLE);
+        mCircularProgressBar = mPlayPauseStopImageContainer.findViewById(
+                R.id.circular_progress_bar);
         mPlayPauseStopImageView.setAction(PlayPauseStopImageView.ACTION_DISABLED);
         mPlayPauseStopImageView.setOnClickListener(this::onPlayPauseStopClicked);
 
+        mShowCircularProgressBar = context.getResources().getBoolean(
+                R.bool.show_circular_progress_bar);
         mIconsColor = context.getResources().getColorStateList(iconColorsId, null);
 
         mSkipPrevButton = createIconButton(context.getDrawable(skipPrevButtonId));
@@ -135,7 +137,7 @@
 
     private ImageButton createIconButton(Drawable icon) {
         ImageButton button = mControlBar.createIconButton(icon);
-        boolean flagInvalidArt = MediaItemMetadata.flagInvalidMediaArt(mContext);
+        boolean flagInvalidArt = CommonFlags.getInstance(mContext).shouldFlagImproperImageRefs();
         if (flagInvalidArt && !(icon instanceof VectorDrawable)) {
             button.setImageTintList(
                     ColorStateList.valueOf(MediaItemMetadata.INVALID_MEDIA_ART_TINT_COLOR));
@@ -150,7 +152,10 @@
 
         boolean hasState = (state != null);
         mPlayPauseStopImageView.setAction(convertMainAction(state));
-        mSpinner.setVisibility(hasState && state.isLoading() ? View.VISIBLE : View.INVISIBLE);
+        boolean isLoading = hasState && state.isLoading();
+        mCircularProgressBar.setVisibility(
+                isLoading || mShowCircularProgressBar ? View.VISIBLE : View.INVISIBLE);
+        mCircularProgressBar.setIndeterminate(isLoading);
 
         // If prev/next is reserved, but not enabled, the icon is displayed as disabled (inactive
         // or grayed out). For example some apps only allow a certain number of skips in a given
@@ -168,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();
@@ -188,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);
@@ -219,7 +212,7 @@
 
     private void updateSpinerColors(MediaSourceColors colors) {
         int color = getMediaSourceColor(colors);
-        mSpinner.setIndeterminateTintList(ColorStateList.valueOf(color));
+        mCircularProgressBar.setIndeterminateTintList(ColorStateList.valueOf(color));
     }
 
     private int getMediaSourceColor(@Nullable MediaSourceColors colors) {
diff --git a/car-media-common/src/com/android/car/media/common/MediaConstants.java b/car-media-common/src/com/android/car/media/common/MediaConstants.java
index f19898c..4465696 100644
--- a/car-media-common/src/com/android/car/media/common/MediaConstants.java
+++ b/car-media-common/src/com/android/car/media/common/MediaConstants.java
@@ -16,13 +16,22 @@
 
 package com.android.car.media.common;
 
-import android.net.Uri;
+import androidx.media.MediaBrowserServiceCompat;
 
 /**
  * Holds constants used when dealing with MediaBrowserServices that support the
  * content style API for media.
  */
 public final class MediaConstants {
+
+    /**
+     * Integer extra indicating the recommended size (in pixels) for media art bitmaps. The value
+     * is passed in the rootHints Bundle of {@link MediaBrowserServiceCompat#onGetRoot} and can be
+     * retrieved with: rootHints.getInt("android.media.extras.MEDIA_ART_SIZE_HINT_PIXELS", 0).
+     */
+    public static final String EXTRA_MEDIA_ART_SIZE_HINT_PIXELS =
+            "android.media.extras.MEDIA_ART_SIZE_HINT_PIXELS";
+
     /**
      * Bundle extra holding the Pending Intent to launch to let users resolve the current error.
      * See {@link #ERROR_RESOLUTION_ACTION_LABEL} for more details.
@@ -120,27 +129,43 @@
     public static final int CONTENT_STYLE_GRID_ITEM_HINT_VALUE = 2;
 
     /**
+     * Value for {@link #CONTENT_STYLE_BROWSABLE_HINT} that hints the corresponding items should be
+     * presented as a "category" list, where media items are browsable and represented by a
+     * meaningful icon.
+     */
+    public static final int CONTENT_STYLE_CATEGORY_LIST_ITEM_HINT_VALUE = 3;
+
+    /**
+     * Value for {@link #CONTENT_STYLE_BROWSABLE_HINT} that hints the corresponding items should be
+     * presented as a "category" grid, where media items are browsable and represented by a
+     * meaningful icon.
+     */
+    public static final int CONTENT_STYLE_CATEGORY_GRID_ITEM_HINT_VALUE = 4;
+
+    /**
+     * These constants are from
+     * @see <a href=https://developer.android.com/training/auto/audio/#required-actions></a>
+     *
+     * @deprecated this flag has been replaced by {@link #PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT}
+     */
+    @Deprecated
+    public static final String SLOT_RESERVATION_SKIP_TO_NEXT =
+            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
+    /**
+     * @deprecated this flag has been replaced by {@link #PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT}
+     */
+    @Deprecated
+    public static final String SLOT_RESERVATION_SKIP_TO_PREV =
+            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
+
+    /**
      * These constants are from
      * @see <a href=https://developer.android.com/training/auto/audio/#required-actions></a>
      */
-    public static final String SLOT_RESERVATION_SKIP_TO_NEXT =
-            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
-    public static final String SLOT_RESERVATION_SKIP_TO_PREV =
-            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
-    public static final String SLOT_RESERVATION_QUEUE =
-            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE";
-
-    /**
-     * Constants for MediaSourceContentProvider
-     */
-    public static final String AUTHORITY_MEDIA_SOURCE = "com.android.car.media.provider";
-    public static final Uri URI_MEDIA_SOURCE =
-            Uri.parse("content://com.android.car.media.provider/media_source");
-    public static final String KEY_PACKAGE_NAME = "package_name";
-
-    /**
-     * Constants for playable status
-     */
+    public static final String PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT =
+            "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT";
+    public static final String PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV =
+            "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS";
 
     /**
      * Bundle extra of type 'boolean' indicating that an item should show the 'explicit' symbol.
diff --git a/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
index b3c9841..317bb7a 100644
--- a/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
+++ b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
@@ -16,48 +16,37 @@
 
 package com.android.car.media.common;
 
-import static android.graphics.Bitmap.Config.ARGB_8888;
-
-import android.annotation.DrawableRes;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.Paint;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.os.SystemProperties;
 import android.support.v4.media.MediaBrowserCompat;
 import android.support.v4.media.MediaDescriptionCompat;
 import android.support.v4.media.MediaMetadataCompat;
 import android.support.v4.media.session.MediaSessionCompat;
-import android.util.Log;
-import android.view.View;
-import android.widget.ImageView;
+import android.text.TextUtils;
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.car.apps.common.UriUtils;
+import com.android.car.apps.common.BitmapUtils;
+import com.android.car.apps.common.CommonFlags;
+import com.android.car.apps.common.imaging.ImageBinder;
+import com.android.car.apps.common.imaging.ImageBinder.PlaceholderType;
 
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.RequestBuilder;
-import com.bumptech.glide.load.DataSource;
-import com.bumptech.glide.load.engine.GlideException;
-import com.bumptech.glide.request.RequestListener;
-import com.bumptech.glide.request.RequestOptions;
-import com.bumptech.glide.request.target.SimpleTarget;
-import com.bumptech.glide.request.target.Target;
-import com.bumptech.glide.request.transition.Transition;
-
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
-import java.util.concurrent.CompletableFuture;
 
 /**
  * Abstract representation of a media item metadata.
@@ -68,15 +57,8 @@
 public class MediaItemMetadata implements Parcelable {
     private static final String TAG = "MediaItemMetadata";
 
-    /**
-     * To red tint invalid art: adb root && adb shell setprop com.android.car.media.FlagInvalidArt 1
-     * Or set R.bool.flag_invalid_media_art to true.
-     */
-    static final String FLAG_INVALID_MEDIA_ART_KEY = "com.android.car.media.FlagInvalidArt";
     static final int INVALID_MEDIA_ART_TINT_COLOR = Color.argb(200, 255, 0, 0);
 
-    private static Boolean sFlagNonLocalMediaArt;
-
     @NonNull
     private final MediaDescriptionCompat mMediaDescription;
     @Nullable
@@ -85,10 +67,8 @@
     private final boolean mIsPlayable;
     private final String mAlbumTitle;
     private final String mArtist;
+    private final ArtworkRef mArtworkKey = new ArtworkRef();
 
-    public MediaItemMetadata(@NonNull MediaDescriptionCompat description) {
-        this(description, null, false, false, null, null);
-    }
 
     /** Creates an instance based on a {@link MediaMetadataCompat} */
     public MediaItemMetadata(@NonNull MediaMetadataCompat metadata) {
@@ -118,21 +98,6 @@
         mArtist = in.readString();
     }
 
-    /**
-     * Creates a clone of this item
-     *
-     * @deprecated this method will be removed as part of b/79089344
-     */
-    @Deprecated
-    public MediaItemMetadata(@NonNull MediaItemMetadata item) {
-        mMediaDescription = item.mMediaDescription;
-        mQueueId = item.mQueueId;
-        mIsBrowsable = item.mIsBrowsable;
-        mIsPlayable = item.mIsPlayable;
-        mAlbumTitle = item.mAlbumTitle;
-        mArtist = item.mArtist;
-    }
-
     @VisibleForTesting
     public MediaItemMetadata(MediaDescriptionCompat description, Long queueId, boolean isBrowsable,
             boolean isPlayable, String albumTitle, String artist) {
@@ -144,6 +109,79 @@
         mArtist = artist;
     }
 
+    /**
+     * The key to access the image to display for this media item.
+     * Implemented as a class so that later we can support showing different images for the same
+     * item (eg: cover and author) by adding other keys.
+     */
+    public class ArtworkRef implements ImageBinder.ImageRef {
+
+        private @Nullable Bitmap getBitmapToFlag(Context context) {
+            CommonFlags flags = CommonFlags.getInstance(context);
+            return (flags.shouldFlagImproperImageRefs() && (mMediaDescription != null))
+                    ? mMediaDescription.getIconBitmap() : null;
+        }
+
+        private int getPlaceholderHash() {
+            // Only the title is reliably populated in metadata, since the album/artist fields
+            // aren't set in the items retrieved from the browse service (only Title/Subtitle).
+            return (getTitle() != null) ? getTitle().hashCode() : 0;
+        }
+
+        @Override
+        public String toString() {
+            return "title: " + getTitle() + " uri: " + getNonEmptyArtworkUri();
+        }
+
+        @Override
+        public @Nullable Uri getImageURI() {
+            return getNonEmptyArtworkUri();
+        }
+
+        @Override
+        public boolean equals(Context context, Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            ArtworkRef other = (ArtworkRef) o;
+
+            Bitmap myBitmap = getBitmapToFlag(context);
+            Bitmap otherBitmap = other.getBitmapToFlag(context);
+            if ((myBitmap != null) || (otherBitmap != null)) {
+                return Objects.equals(myBitmap, otherBitmap);
+            }
+
+            Uri myUri = getImageURI();
+            Uri otherUri = other.getImageURI();
+            if ((myUri != null) || (otherUri != null)) {
+                return Objects.equals(myUri, otherUri);
+            }
+
+            return getPlaceholderHash() == other.getPlaceholderHash();
+        }
+
+        @Override
+        public @Nullable Drawable getImage(Context context) {
+            Bitmap bitmap = getBitmapToFlag(context);
+            if (bitmap != null) {
+                Resources res = context.getResources();
+                return new BitmapDrawable(res, BitmapUtils.createTintedBitmap(bitmap,
+                        context.getColor(
+                                com.android.car.apps.common.R.color.improper_image_refs_tint_color
+                        )));
+            }
+            return null;
+        }
+
+        @Override
+        public Drawable getPlaceholder(Context context, @NonNull PlaceholderType type) {
+            if (type == PlaceholderType.NONE) return null;
+
+            List<Drawable> placeholders = getPlaceHolders(type, context);
+            int random = Math.floorMod(getPlaceholderHash(), placeholders.size());
+            return placeholders.get(random);
+        }
+    }
+
     /** @return media item id */
     @Nullable
     public String getId() {
@@ -183,19 +221,24 @@
         return mQueueId;
     }
 
-    /**
-     * @return album art bitmap, or NULL if this item doesn't have a local album art. In this,
-     * the {@link #getAlbumArtUri()} should be used to obtain a reference to a remote bitmap.
-     */
-    public Bitmap getAlbumArtBitmap() {
-        return mMediaDescription.getIconBitmap();
+
+    public ArtworkRef getArtworkKey() {
+        return mArtworkKey;
     }
 
     /**
-     * @return an {@link Uri} referencing the album art bitmap.
+     * @return a {@link Uri} referencing the artwork's bitmap.
      */
-    public Uri getAlbumArtUri() {
-        return mMediaDescription.getIconUri();
+    private @Nullable Uri getNonEmptyArtworkUri() {
+        Uri uri = mMediaDescription.getIconUri();
+        return (uri != null && !TextUtils.isEmpty(uri.toString())) ? uri : null;
+    }
+
+    /**
+     * @return optional extras that can include extra information about the media item to be played.
+     */
+    public Bundle getExtras() {
+        return mMediaDescription.getExtras();
     }
 
     /**
@@ -216,210 +259,31 @@
                 == MediaDescriptionCompat.STATUS_DOWNLOADED;
     }
 
-    static boolean flagInvalidMediaArt(Context context) {
-        if (sFlagNonLocalMediaArt == null) {
-            Resources res = context.getResources();
-            sFlagNonLocalMediaArt = res.getBoolean(R.bool.flag_invalid_media_art)
-                    || "1".equals(SystemProperties.get(FLAG_INVALID_MEDIA_ART_KEY, "0"));
-        }
-        return sFlagNonLocalMediaArt;
-    }
+    private static Map<PlaceholderType, List<Drawable>> sPlaceHolders = new HashMap<>();
 
-    static Drawable getPlaceholderDrawable(Context context,
-            @NonNull MediaItemMetadata metadata) {
-        TypedArray placeholderImages = context.getResources().obtainTypedArray(
-                R.array.placeholder_images);
-        if (placeholderImages != null && placeholderImages.length() > 0) {
-            // Only the title is reliably populated in metadata, since the album/artist fields
-            // aren't set in the items retrieved from the browse service (only Title/Subtitle).
-            int titleHash = (metadata.getTitle() != null) ? metadata.getTitle().hashCode() : 0;
-            int random = Math.floorMod(titleHash, placeholderImages.length());
-            Drawable placeholder = placeholderImages.getDrawable(random);
+    private static List<Drawable> getPlaceHolders(PlaceholderType type, Context context) {
+        List<Drawable> placeHolders = sPlaceHolders.get(type);
+        if (placeHolders == null) {
+            TypedArray placeholderImages = context.getResources().obtainTypedArray(
+                    type == PlaceholderType.FOREGROUND
+                            ? R.array.placeholder_images : R.array.placeholder_backgrounds);
+
+            if (placeholderImages == null) {
+                throw new NullPointerException("No placeholders for " + type);
+            }
+
+            placeHolders = new ArrayList<>(placeholderImages.length());
+            for (int i = 0; i < placeholderImages.length(); i++) {
+                placeHolders.add(placeholderImages.getDrawable(i));
+            }
             placeholderImages.recycle();
-            return placeholder;
-        }
-        return context.getDrawable(R.drawable.ic_placeholder);
-    }
+            sPlaceHolders.put(type, placeHolders);
 
-    /**
-     * Updates the given {@link ImageView} with the album art of the given media item. This is an
-     * asynchronous operation.
-     * Note: If a view is set using this method, it should also be cleared using this same method.
-     * Given that the loading is asynchronous, missing to use this method for clearing could cause
-     * a delayed request to set an undesired image, or caching entries to be used for images not
-     * longer necessary.
-     *
-     * @param context          {@link Context} used to load resources from
-     * @param metadata         metadata to use, or NULL if the {@link ImageView} should be cleared.
-     * @param imageView        loading target
-     * @param loadingIndicator a drawable resource that would be set into the {@link ImageView}
-     *                         while the image is being downloaded, or 0 if no loading indicator
-     *                         is required.
-     * @param showPlaceholder  whether to show an image placeholder when the image is null.
-     */
-    public static void updateImageView(Context context, @Nullable MediaItemMetadata metadata,
-            ImageView imageView, @DrawableRes int loadingIndicator, boolean showPlaceholder) {
-        Glide.with(context).clear(imageView);
-        imageView.clearColorFilter();
-        if (metadata == null) {
-            imageView.setImageBitmap(null);
-            imageView.setVisibility(View.GONE);
-            return;
-        }
-        boolean flagInvalidArt = flagInvalidMediaArt(context);
-        // Don't even try to get the album art bitmap unless flagging is active.
-        if (flagInvalidArt) {
-            Bitmap image = metadata.getAlbumArtBitmap();
-            if (image != null) {
-                imageView.setImageBitmap(image);
-                imageView.setVisibility(View.VISIBLE);
-                imageView.setColorFilter(INVALID_MEDIA_ART_TINT_COLOR);
-                return;
+            if (sPlaceHolders.size() <= 0) {
+                throw new Resources.NotFoundException("Placeholders should not be empty " + type);
             }
         }
-        Uri imageUri = metadata.getAlbumArtUri();
-        if (imageUri != null) {
-            boolean hasArtwork = false;
-            if (UriUtils.isAndroidResourceUri(imageUri)) {
-                // Glide doesn't support loading resources from other applications
-                Drawable pic = UriUtils.getDrawable(context, UriUtils.getIconResource(imageUri));
-                if (pic != null) {
-                    hasArtwork = true;
-                    imageView.setImageDrawable(pic);
-                } else {
-                    Log.e(TAG, "Unable to load resource " + imageUri);
-                }
-            } else if (flagInvalidArt || UriUtils.isContentUri(imageUri)) {
-                hasArtwork = true;
-                Glide.with(context)
-                        .load(imageUri)
-                        .apply(RequestOptions.placeholderOf(loadingIndicator))
-                        .listener(new RequestListener<Drawable>() {
-                            @Override
-                            public boolean onLoadFailed(@Nullable GlideException e, Object model,
-                                    Target<Drawable> target, boolean isFirstResource) {
-                                showPlaceholderOrHideView(context, metadata, imageView,
-                                        showPlaceholder);
-                                return true;
-                            }
-
-                            @Override
-                            public boolean onResourceReady(Drawable resource, Object model,
-                                    Target<Drawable> target, DataSource dataSource,
-                                    boolean isFirstResource) {
-                                return false;
-                            }
-                        })
-                        .into(imageView);
-                if (!UriUtils.isContentUri(imageUri)) {
-                    imageView.setColorFilter(INVALID_MEDIA_ART_TINT_COLOR);
-                }
-            } else {
-                Log.e(TAG, "unsupported uri: " + imageUri);
-            }
-
-            if (hasArtwork) {
-                imageView.setVisibility(View.VISIBLE);
-                return;
-            }
-        }
-        showPlaceholderOrHideView(context, metadata, imageView, showPlaceholder);
-    }
-
-    private static void showPlaceholderOrHideView(Context context,
-            @Nullable MediaItemMetadata metadata, ImageView imageView, boolean showPlaceholder) {
-        if (showPlaceholder) {
-            imageView.setImageDrawable(getPlaceholderDrawable(context, metadata));
-            imageView.setVisibility(View.VISIBLE);
-        } else {
-            imageView.setImageBitmap(null);
-            imageView.setVisibility(View.GONE);
-        }
-    }
-
-    /**
-     * Loads the album art of this media item asynchronously. The loaded image will be scaled to
-     * fit into the given view size.
-     * Using {@link #updateImageView(Context, MediaItemMetadata, ImageView, int)} method is
-     * preferred. Only use this method if you are going to apply transformations to the loaded
-     * image.
-     *
-     * @param width  desired width (should be > 0)
-     * @param height desired height (should be > 0)
-     * @param fit    whether the image should be scaled to fit (fitCenter), or it should be cropped
-     *               (centerCrop).
-     * @return a {@link CompletableFuture} that will be completed once the image is loaded, or the
-     * loading fails.
-     */
-    public CompletableFuture<Bitmap> getAlbumArt(Context context, int width, int height,
-            boolean fit) {
-        boolean flagInvalidArt = flagInvalidMediaArt(context);
-        // Don't even try to get the album art bitmap unless flagging is active.
-        if (flagInvalidArt) {
-            Bitmap image = getAlbumArtBitmap();
-            if (image != null) {
-                return CompletableFuture.completedFuture(flagBitmap(image));
-            }
-        }
-        Uri imageUri = getAlbumArtUri();
-        if (imageUri != null) {
-            if (UriUtils.isAndroidResourceUri(imageUri)) {
-                // Glide doesn't support loading resources for other applications...
-                Drawable pic = UriUtils.getDrawable(context, UriUtils.getIconResource(imageUri));
-                if (pic != null) {
-                    Bitmap bitmap = Bitmap.createBitmap(width, height, ARGB_8888);
-                    Canvas canvas = new Canvas(bitmap);
-                    pic.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
-                    pic.draw(canvas);
-                    return CompletableFuture.completedFuture(bitmap);
-                } else {
-                    String errorMessage = "Unable to load resource " + imageUri;
-                    Log.e(TAG, errorMessage);
-                    return CompletableFuture.failedFuture(new Exception(errorMessage));
-                }
-            } else if (flagInvalidArt || UriUtils.isContentUri(imageUri)) {
-                CompletableFuture<Bitmap> bitmapCompletableFuture = new CompletableFuture<>();
-                RequestBuilder<Bitmap> builder = Glide.with(context)
-                        .asBitmap()
-                        .load(getAlbumArtUri());
-                if (fit) {
-                    builder = builder.apply(RequestOptions.fitCenterTransform());
-                } else {
-                    builder = builder.apply(RequestOptions.centerCropTransform());
-                }
-                Target<Bitmap> target = new SimpleTarget<Bitmap>(width, height) {
-                    @Override
-                    public void onResourceReady(@NonNull Bitmap bitmap,
-                            @Nullable Transition<? super Bitmap> transition) {
-                        if (!UriUtils.isContentUri(imageUri)) {
-                            bitmap = flagBitmap(bitmap);
-                        }
-                        bitmapCompletableFuture.complete(bitmap);
-                    }
-
-                    @Override
-                    public void onLoadFailed(@Nullable Drawable errorDrawable) {
-                        bitmapCompletableFuture.completeExceptionally(
-                                new IllegalStateException("Unknown error"));
-                    }
-                };
-                builder.into(target);
-                return bitmapCompletableFuture;
-            } else {
-                String errorMessage = "unsupported uri: \n" + imageUri;
-                Log.e(TAG, errorMessage);
-                return CompletableFuture.failedFuture(new Exception(errorMessage));
-            }
-        }
-        return CompletableFuture.completedFuture(null);
-    }
-
-    private static Bitmap flagBitmap(Bitmap image) {
-        Bitmap clone = Bitmap.createBitmap(image.getWidth(), image.getHeight(), ARGB_8888);
-        Canvas canvas = new Canvas(clone);
-        canvas.drawBitmap(image, 0f, 0f, new Paint());
-        canvas.drawColor(INVALID_MEDIA_ART_TINT_COLOR);
-        return clone;
+        return placeHolders;
     }
 
     public boolean isBrowsable() {
@@ -492,7 +356,7 @@
                 && Objects.equals(getSubtitle(), that.getSubtitle())
                 && Objects.equals(getAlbumTitle(), that.getAlbumTitle())
                 && Objects.equals(getArtist(), that.getArtist())
-                && Objects.equals(getAlbumArtUri(), that.getAlbumArtUri())
+                && Objects.equals(getNonEmptyArtworkUri(), that.getNonEmptyArtworkUri())
                 && Objects.equals(mQueueId, that.mQueueId);
     }
 
diff --git a/car-media-common/src/com/android/car/media/common/MetadataController.java b/car-media-common/src/com/android/car/media/common/MetadataController.java
index ce3107a..49e5a26 100644
--- a/car-media-common/src/com/android/car/media/common/MetadataController.java
+++ b/car-media-common/src/com/android/car/media/common/MetadataController.java
@@ -21,7 +21,9 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.Context;
 import android.text.TextUtils;
+import android.util.Size;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.SeekBar;
@@ -29,6 +31,7 @@
 
 import androidx.lifecycle.LifecycleOwner;
 
+import com.android.car.apps.common.imaging.ImageViewBinder;
 import com.android.car.apps.common.util.ViewUtils;
 import com.android.car.media.common.playback.PlaybackViewModel;
 
@@ -37,6 +40,7 @@
  */
 public class MetadataController {
     private PlaybackViewModel.PlaybackController mController;
+    private final ImageViewBinder<MediaItemMetadata.ArtworkRef> mAlbumArtBinder;
 
     private boolean mTrackingTouch;
     private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener =
@@ -80,14 +84,18 @@
      * @param maxTime           Displays the track's duration as text. May be {@code null}.
      * @param seekBar           Displays the track's progress visually. May be {@code null}.
      * @param albumArt          Displays the track's album art. May be {@code null}.
-     * @param albumArtSizePx    Size of track's album art.
+     * @param maxArtSize        Maximum size of the track's album art.
      */
     public MetadataController(@NonNull LifecycleOwner lifecycleOwner,
             @NonNull PlaybackViewModel playbackViewModel, @NonNull TextView title,
             @Nullable TextView artist, @Nullable TextView albumTitle,
             @Nullable TextView outerSeparator, @Nullable TextView currentTime,
             @Nullable TextView innerSeparator, @Nullable TextView maxTime,
-            @Nullable SeekBar seekBar, @Nullable ImageView albumArt, int albumArtSizePx) {
+            @Nullable SeekBar seekBar, @Nullable ImageView albumArt, Size maxArtSize) {
+
+        Context context = title.getContext();
+        mAlbumArtBinder = new ImageViewBinder<>(maxArtSize, albumArt);
+
         playbackViewModel.getPlaybackController().observe(lifecycleOwner,
                 controller -> mController = controller);
         playbackViewModel.getMetadata().observe(lifecycleOwner,
@@ -101,7 +109,7 @@
                     }
                     CharSequence titleName = metadata.getTitle();
                     if (TextUtils.isEmpty(titleName)) {
-                        titleName = title.getContext().getString(R.string.metadata_default_title);
+                        titleName = context.getString(R.string.metadata_default_title);
                     }
                     title.setText(titleName);
                     ViewUtils.setVisible(title, true);
@@ -111,23 +119,10 @@
                         artist.setText(artistName);
                         ViewUtils.setVisible(artist, !TextUtils.isEmpty(artistName));
                     }
-                    if (albumArt != null) {
-                        // TODO(b/130444922): Clean up bitmap fetching code
-                        metadata.getAlbumArt(playbackViewModel.getApplication(),
-                                albumArtSizePx, albumArtSizePx, true).whenComplete(
-                                        (result, throwable) -> {
-                                            if (throwable == null && result != null) {
-                                                albumArt.setImageBitmap(result);
-                                            } else {
-                                                albumArt.setImageDrawable(
-                                                        MediaItemMetadata.getPlaceholderDrawable(
-                                                                playbackViewModel.getApplication(),
-                                                                metadata));
-                                            }
-                                        }
-                        );
-                        ViewUtils.setVisible(albumArt, true);
-                    }
+
+                    ViewUtils.setVisible(albumArt, true);
+
+                    mAlbumArtBinder.setImage(context, metadata.getArtworkKey());
                 });
 
         playbackViewModel.getProgress().observe(lifecycleOwner,
diff --git a/car-media-common/src/com/android/car/media/common/MinimizedPlaybackControlBar.java b/car-media-common/src/com/android/car/media/common/MinimizedPlaybackControlBar.java
index e316bc0..7ce34b2 100644
--- a/car-media-common/src/com/android/car/media/common/MinimizedPlaybackControlBar.java
+++ b/car-media-common/src/com/android/car/media/common/MinimizedPlaybackControlBar.java
@@ -17,8 +17,8 @@
 package com.android.car.media.common;
 
 import android.content.Context;
-import android.content.res.ColorStateList;
 import android.util.AttributeSet;
+import android.util.Size;
 import android.widget.ProgressBar;
 
 import androidx.annotation.NonNull;
@@ -31,15 +31,19 @@
  * This is a CarControlBar used for displaying Media content, including metadata for the currently
  * playing song and basic controls.
  */
-public class MinimizedPlaybackControlBar extends MinimizedControlBar implements PlaybackControls {
+public class MinimizedPlaybackControlBar extends MinimizedControlBar {
 
     private static final String TAG = "Media.ControlBar";
 
     private MediaButtonController mMediaButtonController;
     private MetadataController mMetadataController;
-    private ProgressBar mProgressBar;
+    private ProgressBar mLinearProgressBar;
+    private ProgressBar mCircularProgressBar;
     private PlaybackViewModel mPlaybackViewModel;
 
+    private boolean mShowLinearProgressBar;
+    private boolean mShowCircularProgressBar;
+
     public MinimizedPlaybackControlBar(Context context) {
         this(context, null);
     }
@@ -55,43 +59,28 @@
 
     private void init(Context context) {
         mMediaButtonController = new MediaButtonController(context, this,
-                R.color.playback_control_color, R.layout.minimized_play_pause_stop_button_layout,
+                R.color.playback_control_color, R.layout.play_pause_stop_button_layout,
                 R.drawable.ic_skip_previous, R.drawable.ic_skip_next);
-        mProgressBar = findViewById(R.id.progress_bar);
+
+        mShowLinearProgressBar = context.getResources().getBoolean(R.bool.show_linear_progress_bar);
+        mLinearProgressBar = findViewById(R.id.linear_progress_bar);
+
+        mShowCircularProgressBar = context.getResources().getBoolean(
+                R.bool.show_circular_progress_bar);
+        mCircularProgressBar = findViewById(R.id.circular_progress_bar);
     }
 
-    @Override
-    public void setModel(@NonNull PlaybackViewModel model, @NonNull LifecycleOwner owner) {
+    /** Connects the bar to the {@link PlaybackViewModel}. */
+    public void setModel(@NonNull PlaybackViewModel model, @NonNull LifecycleOwner owner,
+            @NonNull Size maxArtSize) {
         mMediaButtonController.setModel(model, owner);
         mMetadataController = new MetadataController(owner, model,
-                mTitle, mSubtitle, null, null, null, null, null, null,
-                mContentTile, getContext().getResources().getDimensionPixelSize(
-                R.dimen.minimized_control_bar_content_tile_size));
-
+                mTitle, mSubtitle, null, null, null, null, null, null, mContentTile, maxArtSize);
         mPlaybackViewModel = model;
-        if (mProgressBar != null) {
-            boolean useMediaSourceColor =
-                    getContext().getResources().getBoolean(
-                            R.bool.use_media_source_color_for_minimized_progress_bar);
-            int defaultColor = getContext().getResources().getColor(
-                    R.color.minimized_progress_bar_highlight, null);
-            if (useMediaSourceColor) {
-                mPlaybackViewModel.getMediaSourceColors().observe(owner,
-                        sourceColors -> {
-                            int color = sourceColors != null ? sourceColors.getAccentColor(
-                                    defaultColor)
-                                    : defaultColor;
-                            mProgressBar.setProgressTintList(ColorStateList.valueOf(color));
-                        });
-            } else {
-                mProgressBar.setProgressTintList(ColorStateList.valueOf(defaultColor));
-            }
 
-            mPlaybackViewModel.getProgress().observe(owner,
-                    progress -> {
-                        mProgressBar.setProgress((int) progress.getProgress());
-                        mProgressBar.setMax((int) progress.getMaxProgress());
-                    });
-        }
+        ControlBarHelper.initProgressBar(getContext(), owner, mPlaybackViewModel,
+                mLinearProgressBar, mShowLinearProgressBar);
+        ControlBarHelper.initProgressBar(getContext(), owner, mPlaybackViewModel,
+                mCircularProgressBar, mShowCircularProgressBar);
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackControls.java b/car-media-common/src/com/android/car/media/common/PlaybackControls.java
deleted file mode 100644
index 13b9c64..0000000
--- a/car-media-common/src/com/android/car/media/common/PlaybackControls.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.android.car.media.common;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.LifecycleOwner;
-
-import com.android.car.media.common.playback.PlaybackViewModel;
-
-/**
- * Custom view that can be used to display playback controls. It accepts a {@link PlaybackViewModel}
- * as its data source, automatically reacting to changes in playback state.
- */
-public interface PlaybackControls {
-    /**
-     * Sets the {@link PlaybackViewModel} to use as the view model for this view.
-     */
-    void setModel(@NonNull PlaybackViewModel model, @NonNull LifecycleOwner lifecycleOwner);
-}
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java b/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java
index 6d3d8db..9fe7523 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.util.AttributeSet;
 import android.widget.ImageButton;
+import android.widget.ProgressBar;
 
 import androidx.annotation.NonNull;
 import androidx.lifecycle.LifecycleOwner;
@@ -27,15 +28,17 @@
 import com.android.car.media.common.playback.PlaybackViewModel;
 
 /**
- * Implementation of {@link PlaybackControls} that uses the {@link ControlBar}
+ * Basic playback control bar (doesn't display any metadata).
  */
-public class PlaybackControlsActionBar extends ControlBar implements PlaybackControls {
-    private static final String TAG = "PlaybackView";
+public class PlaybackControlsActionBar extends ControlBar {
 
     private ImageButton mOverflowButton;
+    private ProgressBar mCircularProgressBar;
 
     private MediaButtonController mMediaButtonController;
 
+    private boolean mShowCircularProgressBar;
+
     /** Creates a {@link PlaybackControlsActionBar} view */
     public PlaybackControlsActionBar(Context context) {
         this(context, null, 0, 0);
@@ -65,12 +68,17 @@
         setExpandCollapseView(mOverflowButton);
 
         mMediaButtonController = new MediaButtonController(context, this,
-                R.color.playback_control_color, R.layout.full_play_pause_stop_button_layout,
+                R.color.playback_control_color, R.layout.play_pause_stop_button_layout,
                 R.drawable.ic_skip_previous, R.drawable.ic_skip_next);
+
+        mShowCircularProgressBar = context.getResources().getBoolean(
+                R.bool.show_circular_progress_bar);
+        mCircularProgressBar = findViewById(R.id.circular_progress_bar);
     }
 
-    @Override
     public void setModel(@NonNull PlaybackViewModel model, @NonNull LifecycleOwner owner) {
         mMediaButtonController.setModel(model, owner);
+        ControlBarHelper.initProgressBar(getContext(), owner, model, mCircularProgressBar,
+                mShowCircularProgressBar);
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
index 4d2c29f..36f428d 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
@@ -23,9 +23,11 @@
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.os.Bundle;
+import android.util.Size;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
@@ -36,23 +38,24 @@
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.ViewModelProviders;
 
+import com.android.car.apps.common.BitmapUtils;
 import com.android.car.apps.common.CrossfadeImageView;
+import com.android.car.apps.common.imaging.ImageBinder;
+import com.android.car.apps.common.imaging.ImageBinder.PlaceholderType;
 import com.android.car.apps.common.util.ViewUtils;
-import com.android.car.media.common.playback.AlbumArtLiveData;
 import com.android.car.media.common.playback.PlaybackViewModel;
 import com.android.car.media.common.source.MediaSource;
 import com.android.car.media.common.source.MediaSourceViewModel;
 
-import com.bumptech.glide.request.target.Target;
-
 /**
  * {@link Fragment} that can be used to display and control the currently playing media item. Its
  * requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the hosting
  * application.
  */
 public class PlaybackFragment extends Fragment {
-
+    private Intent mAppSelectorIntent;
     private MediaSourceViewModel mMediaSourceViewModel;
+    private ImageBinder<MediaItemMetadata.ArtworkRef> mAlbumArtBinder;
 
     @Nullable
     @Override
@@ -61,6 +64,7 @@
         FragmentActivity activity = requireActivity();
         PlaybackViewModel playbackViewModel = PlaybackViewModel.get(activity.getApplication());
         mMediaSourceViewModel = MediaSourceViewModel.get(activity.getApplication());
+        mAppSelectorIntent = MediaSource.getSourceSelectorIntent(getContext(), true);
 
         ViewModel innerViewModel = ViewModelProviders.of(activity).get(ViewModel.class);
         innerViewModel.init(mMediaSourceViewModel, playbackViewModel);
@@ -82,15 +86,29 @@
         TextView subtitle = view.findViewById(R.id.subtitle);
         innerViewModel.getSubtitle().observe(getViewLifecycleOwner(), subtitle::setText);
 
+        ImageView appIcon = view.findViewById(R.id.app_icon);
+        innerViewModel.getAppIcon().observe(getViewLifecycleOwner(), appIcon::setImageBitmap);
+
         CrossfadeImageView albumBackground = view.findViewById(R.id.album_background);
-        innerViewModel.getAlbumArt().observe(getViewLifecycleOwner(),
-                albumArt -> albumBackground.setImageBitmap(albumArt, true));
         albumBackground.setOnClickListener(
                 // Let the Media center trampoline figure out what to open.
                 v -> startActivity(new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE)));
 
-        MediaAppSelectorWidget appSelector = view.findViewById(R.id.app_switch_container);
-        appSelector.setFragmentActivity(getActivity());
+        int max = activity.getResources().getInteger(R.integer.media_items_bitmap_max_size_px);
+        Size maxArtSize = new Size(max, max);
+        mAlbumArtBinder = new ImageBinder<>(PlaceholderType.FOREGROUND, maxArtSize,
+                drawable -> {
+                    Bitmap bitmap = (drawable != null)
+                            ? BitmapUtils.fromDrawable(drawable, maxArtSize) : null;
+                    albumBackground.setImageBitmap(bitmap, true);
+                });
+
+        playbackViewModel.getMetadata().observe(getViewLifecycleOwner(),
+                item -> mAlbumArtBinder.setImage(PlaybackFragment.this.getContext(),
+                        item != null ? item.getArtworkKey() : null));
+        View appSelector = view.findViewById(R.id.app_selector_container);
+        appSelector.setVisibility(mAppSelectorIntent != null ? View.VISIBLE : View.GONE);
+        appSelector.setOnClickListener(e -> getContext().startActivity(mAppSelectorIntent));
 
         return view;
     }
@@ -105,7 +123,6 @@
         private LiveData<Bitmap> mAppIcon;
         private LiveData<CharSequence> mTitle;
         private LiveData<CharSequence> mSubtitle;
-        private LiveData<Bitmap> mAlbumArt;
 
         private PlaybackViewModel mPlaybackViewModel;
         private MediaSourceViewModel mMediaSourceViewModel;
@@ -122,13 +139,10 @@
             mPlaybackViewModel = playbackViewModel;
             mMediaSourceViewModel = mediaSourceViewModel;
             mMediaSource = mMediaSourceViewModel.getPrimaryMediaSource();
-            mAppName = mapNonNull(mMediaSource, MediaSource::getName);
+            mAppName = mapNonNull(mMediaSource, MediaSource::getDisplayName);
             mAppIcon = mapNonNull(mMediaSource, MediaSource::getRoundPackageIcon);
             mTitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getTitle);
             mSubtitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getArtist);
-            mAlbumArt = AlbumArtLiveData.getAlbumArt(getApplication(),
-                    Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, false,
-                    playbackViewModel.getMetadata());
         }
 
         LiveData<CharSequence> getAppName() {
@@ -146,9 +160,5 @@
         LiveData<CharSequence> getSubtitle() {
             return mSubtitle;
         }
-
-        LiveData<Bitmap> getAlbumArt() {
-            return mAlbumArt;
-        }
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java b/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
index 829b644..dea0d6a 100644
--- a/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
+++ b/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
@@ -17,7 +17,6 @@
 package com.android.car.media.common.browse;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.os.Bundle;
 import android.os.Handler;
 import android.support.v4.media.MediaBrowserCompat;
@@ -53,7 +52,7 @@
 
     private ChildrenSubscription mSubscription;
 
-    BrowsedMediaItems(@NonNull MediaBrowserCompat mediaBrowser, @Nullable String parentId) {
+    BrowsedMediaItems(@NonNull MediaBrowserCompat mediaBrowser, @NonNull String parentId) {
         mBrowser = mediaBrowser;
         mParentId = parentId;
     }
@@ -61,10 +60,7 @@
     @Override
     protected void onActive() {
         super.onActive();
-        String rootNode = mBrowser.getRoot();
-        String itemId = mParentId != null ? mParentId : rootNode;
-
-        mSubscription = new ChildrenSubscription(itemId);
+        mSubscription = new ChildrenSubscription(mParentId);
         mSubscription.start(CHILDREN_SUBSCRIPTION_RETRIES, CHILDREN_SUBSCRIPTION_RETRY_TIME_MS);
     }
 
diff --git a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java
index 74143c1..5a4626a 100644
--- a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java
+++ b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModel.java
@@ -112,7 +112,7 @@
          * {@link #getBrowsedMediaItems()}.
          */
         @UiThread
-        void setCurrentBrowseId(@Nullable String browseId);
+        void setCurrentBrowseId(@NonNull String browseId);
 
         /**
          * Set the current item to be searched for. If available, the list of items will be emitted
@@ -137,61 +137,16 @@
          */
         @NonNull
         public static MediaBrowserViewModel.WithMutableBrowseId getInstanceWithMediaBrowser(
+                @NonNull String key,
                 @NonNull ViewModelProvider viewModelProvider,
                 @NonNull LiveData<MediaBrowserCompat> mediaBrowser) {
-            MediaBrowserViewModelImpl viewModel = viewModelProvider.get(
-                    MediaBrowserViewModelImpl.class);
+            MutableMediaBrowserViewModel viewModel =
+                    viewModelProvider.get(key, MutableMediaBrowserViewModel.class);
             initMediaBrowser(mediaBrowser, viewModel);
             return viewModel;
         }
 
         /**
-         * Fetch an initialized {@link MediaBrowserViewModel.WithMutableBrowseId}. It will get its
-         * media browser from the {@link MediaSourceViewModel} provided by {@code
-         * viewModelProvider}.
-         *
-         *
-         * @param mediaSourceVM     the {@link MediaSourceViewModel} singleton.
-         * @param viewModelProvider the ViewModelProvider to load ViewModels from.
-         * @param key               a key to decide which instance of the ViewModel to fetch.
-         *                          Subsequent calls with the same key will return the same
-         *                          instance.
-         * @return an initialized MediaBrowserViewModel.WithMutableBrowseId for the given key.
-         * @see ViewModelProvider#get(String, Class)
-         */
-        @NonNull
-        public static MediaBrowserViewModel.WithMutableBrowseId getInstanceForKey(
-                MediaSourceViewModel mediaSourceVM, @NonNull ViewModelProvider viewModelProvider,
-                @NonNull String key) {
-            MediaBrowserViewModelImpl viewModel = viewModelProvider.get(key,
-                    MediaBrowserViewModelImpl.class);
-            initMediaBrowser(mediaSourceVM.getConnectedMediaBrowser(), viewModel);
-            return viewModel;
-        }
-
-        /**
-         * Fetch an initialized {@link MediaBrowserViewModel}. It will get its media browser from
-         * the {@link MediaSourceViewModel} provided by {@code viewModelProvider}. It will already
-         * be configured to browse {@code browseId}.
-         *
-         *
-         * @param mediaSourceVM     the {@link MediaSourceViewModel} singleton.
-         * @param viewModelProvider the ViewModelProvider to load ViewModels from.
-         * @param browseId          the browseId to browse. This will also serve as the key for
-         *                          fetching the ViewModel.
-         * @return an initialized MediaBrowserViewModel configured to browse the specified browseId.
-         */
-        @NonNull
-        public static MediaBrowserViewModel getInstanceForBrowseId(
-                MediaSourceViewModel mediaSourceVM, @NonNull ViewModelProvider viewModelProvider,
-                @NonNull String browseId) {
-            MediaBrowserViewModel.WithMutableBrowseId viewModel =
-                    getInstanceForKey(mediaSourceVM, viewModelProvider, browseId);
-            viewModel.setCurrentBrowseId(browseId);
-            return viewModel;
-        }
-
-        /**
          * Fetch an initialized {@link MediaBrowserViewModel}. It will get its media browser from
          * the {@link MediaSourceViewModel} provided by {@code viewModelProvider}. It will already
          * be configured to browse the root of the browser.
@@ -203,9 +158,9 @@
         @NonNull
         public static MediaBrowserViewModel getInstanceForBrowseRoot(
                 MediaSourceViewModel mediaSourceVM, @NonNull ViewModelProvider viewModelProvider) {
-            MediaBrowserViewModel.WithMutableBrowseId viewModel =
-                    getInstanceForKey(mediaSourceVM, viewModelProvider, KEY_BROWSER_ROOT);
-            viewModel.setCurrentBrowseId(null);
+            RootMediaBrowserViewModel viewModel =
+                    viewModelProvider.get(KEY_BROWSER_ROOT, RootMediaBrowserViewModel.class);
+            initMediaBrowser(mediaSourceVM.getConnectedMediaBrowser(), viewModel);
             return viewModel;
         }
 
diff --git a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java
index 71a1e56..7d47ba7 100644
--- a/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java
+++ b/car-media-common/src/com/android/car/media/common/browse/MediaBrowserViewModelImpl.java
@@ -25,7 +25,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.UiThread;
 import android.app.Application;
 import android.os.Bundle;
 import android.support.v4.media.MediaBrowserCompat;
@@ -49,14 +48,15 @@
  * obtained via {@link MediaBrowserViewModel.Factory}
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-public class MediaBrowserViewModelImpl extends AndroidViewModel implements
-        MediaBrowserViewModel.WithMutableBrowseId {
+class MediaBrowserViewModelImpl extends AndroidViewModel implements MediaBrowserViewModel {
+
+    private final boolean mIsRoot;
 
     private final SwitchingLiveData<MediaBrowserCompat> mMediaBrowserSwitch =
             SwitchingLiveData.newInstance();
 
-    private final MutableLiveData<String> mCurrentBrowseId = new MutableLiveData<>();
-    private final MutableLiveData<String> mCurrentSearchQuery = dataOf(null);
+    final MutableLiveData<String> mCurrentBrowseId = dataOf(null);
+    final MutableLiveData<String> mCurrentSearchQuery = dataOf(null);
     private final LiveData<MediaBrowserCompat> mConnectedMediaBrowser =
             map(mMediaBrowserSwitch.asLiveData(), MediaBrowserViewModelImpl::requireConnected);
 
@@ -67,9 +67,11 @@
 
     private final LiveData<String> mPackageName;
 
-    public MediaBrowserViewModelImpl(@NonNull Application application) {
+    MediaBrowserViewModelImpl(@NonNull Application application, boolean isRoot) {
         super(application);
 
+        mIsRoot = isRoot;
+
         mPackageName = map(mConnectedMediaBrowser,
                 mediaBrowser -> {
                     if (mediaBrowser == null) return null;
@@ -78,10 +80,14 @@
 
         mBrowsedMediaItems =
                 loadingSwitchMap(pair(mConnectedMediaBrowser, mCurrentBrowseId),
-                        split((mediaBrowser, browseId) ->
-                                mediaBrowser == null
-                                        ? null
-                                        : new BrowsedMediaItems(mediaBrowser, browseId)));
+                        split((mediaBrowser, browseId) -> {
+                            if (mediaBrowser == null || (!mIsRoot && browseId == null)) {
+                                return null;
+                            }
+
+                            String parentId = (mIsRoot) ? mediaBrowser.getRoot() : browseId;
+                            return new BrowsedMediaItems(mediaBrowser, parentId);
+                        }));
         mSearchedMediaItems =
                 loadingSwitchMap(pair(mConnectedMediaBrowser, mCurrentSearchQuery),
                         split((mediaBrowser, query) ->
@@ -143,26 +149,6 @@
         return mMediaBrowserSwitch.getSource();
     }
 
-    /**
-     * Set the current item to be browsed. If available, the list of items will be emitted by {@link
-     * #getBrowsedMediaItems()}.
-     */
-    @UiThread
-    @Override
-    public void setCurrentBrowseId(@Nullable String browseId) {
-        mCurrentBrowseId.setValue(browseId);
-    }
-
-    /**
-     * Set the current item to be searched for. If available, the list of items will be emitted
-     * by {@link #getBrowsedMediaItems()}.
-     */
-    @UiThread
-    @Override
-    public void search(@Nullable String query) {
-        mCurrentSearchQuery.setValue(query);
-    }
-
     @Override
     public LiveData<String> getPackageName() {
         return mPackageName;
diff --git a/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java b/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java
new file mode 100644
index 0000000..482efdd
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/browse/MutableMediaBrowserViewModel.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.media.common.browse;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.app.Application;
+
+import androidx.annotation.RestrictTo;
+
+/** This isn't a comment. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class MutableMediaBrowserViewModel extends MediaBrowserViewModelImpl implements
+        MediaBrowserViewModel.WithMutableBrowseId {
+    public MutableMediaBrowserViewModel(@NonNull Application application) {
+        super(application, /*isRoot*/ false);
+    }
+
+    @UiThread
+    @Override
+    public void setCurrentBrowseId(@NonNull String browseId) {
+        super.mCurrentBrowseId.setValue(browseId);
+    }
+
+    @UiThread
+    @Override
+    public void search(@Nullable String query) {
+        super.mCurrentSearchQuery.setValue(query);
+    }
+}
diff --git a/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java b/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java
new file mode 100644
index 0000000..1edb484
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/browse/RootMediaBrowserViewModel.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.media.common.browse;
+
+import android.annotation.NonNull;
+import android.app.Application;
+
+import androidx.annotation.RestrictTo;
+
+/** This isn't a comment. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class RootMediaBrowserViewModel extends MediaBrowserViewModelImpl {
+    public RootMediaBrowserViewModel(@NonNull Application application) {
+        super(application, /*isRoot*/ true);
+    }
+}
diff --git a/car-media-common/src/com/android/car/media/common/playback/AlbumArtLiveData.java b/car-media-common/src/com/android/car/media/common/playback/AlbumArtLiveData.java
deleted file mode 100644
index 9a3589c..0000000
--- a/car-media-common/src/com/android/car/media/common/playback/AlbumArtLiveData.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.car.media.common.playback;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MediatorLiveData;
-
-import com.android.car.media.common.MediaItemMetadata;
-
-import java.util.concurrent.CompletableFuture;
-
-/**
- * LiveData class for loading album art from a MediaItemMetadata. This type should not be used
- * directly; instances should be created via {@link #getAlbumArt(Context, int, int, boolean,
- * LiveData)}.
- */
-public class AlbumArtLiveData extends MediatorLiveData<Bitmap> {
-
-    /**
-     * Returns a new LiveData that emits a Bitmap representation of the {@link MediaItemMetadata}'s
-     * album art. While the MediaItemMetadata returns a {@link CompletableFuture}, this LiveData
-     * only updates once the future has completed. If it completes exceptionally or the source emits
-     * {@code null}, the LiveData's value is set to {@code null}.
-     *
-     * @see MediaItemMetadata#getAlbumArt(Context, int, int, boolean)
-     */
-    public static LiveData<Bitmap> getAlbumArt(Context context, int width, int height, boolean fit,
-            LiveData<MediaItemMetadata> source) {
-        return new AlbumArtLiveData(context, width, height, fit, source);
-    }
-
-    private final Context mContext;
-    private final int mWidth;
-    private final int mHeight;
-    private final boolean mFit;
-    private CompletableFuture<Bitmap> mFuture;
-
-    private AlbumArtLiveData(Context context, int width, int height, boolean fit,
-            LiveData<MediaItemMetadata> source) {
-        mContext = context.getApplicationContext();
-        mWidth = width;
-        mHeight = height;
-        mFit = fit;
-        addSource(source, this::update);
-    }
-
-    private void update(MediaItemMetadata metadata) {
-        if (mFuture != null && !mFuture.isDone()) {
-            mFuture.cancel(true);
-        }
-        if (metadata == null) {
-            setValue(null);
-            mFuture = null;
-        } else {
-            mFuture = metadata.getAlbumArt(mContext, mWidth, mHeight, mFit);
-            mFuture.whenComplete((result, throwable) -> {
-                if (throwable != null) {
-                    postValue(null);
-                } else {
-                    postValue(result);
-                }
-            });
-        }
-    }
-}
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 e085c00..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
@@ -335,7 +335,9 @@
 
         /** Returns true if there's enough information in the state to show a UI for it. */
         public boolean shouldDisplay() {
-            return (mMetadata != null) || (getMainAction() != ACTION_DISABLED);
+            // STATE_NONE means no content to play.
+            return mState.getState() != PlaybackStateCompat.STATE_NONE && ((mMetadata != null) || (
+                    getMainAction() != ACTION_DISABLED));
         }
 
         /** Returns the main action. */
@@ -373,6 +375,13 @@
         }
 
         /**
+         * Returns the currently supported playback actions
+         */
+        public long getSupportedActions() {
+            return mState.getActions();
+        }
+
+        /**
          * Returns the duration of the media item in milliseconds. The current position in this
          * duration can be obtained by calling {@link #getProgress()}.
          */
@@ -406,8 +415,10 @@
         /** Returns whether the media source requires reserved space for the skip to next action. */
         public boolean isSkipNextReserved() {
             return mMediaController.getExtras() != null
-                    && mMediaController.getExtras().getBoolean(
-                    MediaConstants.SLOT_RESERVATION_SKIP_TO_NEXT);
+                    && (mMediaController.getExtras().getBoolean(
+                    MediaConstants.SLOT_RESERVATION_SKIP_TO_NEXT)
+                    || mMediaController.getExtras().getBoolean(
+                    MediaConstants.PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT));
         }
 
         /**
@@ -415,8 +426,10 @@
          */
         public boolean iSkipPreviousReserved() {
             return mMediaController.getExtras() != null
-                    && mMediaController.getExtras().getBoolean(
-                    MediaConstants.SLOT_RESERVATION_SKIP_TO_PREV);
+                    && (mMediaController.getExtras().getBoolean(
+                    MediaConstants.SLOT_RESERVATION_SKIP_TO_PREV)
+                    || mMediaController.getExtras().getBoolean(
+                    MediaConstants.PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV));
         }
 
         /** Returns whether the media source is loading (e.g.: buffering, connecting, etc.). */
@@ -598,12 +611,13 @@
         }
 
         /**
-         * Starts playing a given media item. This id corresponds to {@link
-         * MediaItemMetadata#getId()}.
+         * Starts playing a given media item.
          */
-        public void playItem(String mediaItemId) {
+        public void playItem(MediaItemMetadata item) {
             if (mMediaController != null) {
-                mMediaController.getTransportControls().playFromMediaId(mediaItemId, null);
+                // 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-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java b/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
index 36f90c8..b16164a 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
@@ -22,9 +22,12 @@
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
+import android.os.Bundle;
 import android.support.v4.media.MediaBrowserCompat;
 import android.util.Log;
 
+import com.android.car.media.common.MediaConstants;
+
 /**
  * A helper class to connect to a single {@link MediaBrowserCompat}. Connecting to a new one
  * automatically disconnects the previous browser. Changes of the currently connected browser are
@@ -43,6 +46,7 @@
 
     private final Context mContext;
     private final Callback mCallback;
+    private final int mMaxBitmapSizePx;
 
     @Nullable private ComponentName mBrowseService;
     @Nullable private MediaBrowserCompat mBrowser;
@@ -55,6 +59,8 @@
     MediaBrowserConnector(@NonNull Context context, @NonNull Callback callback) {
         mContext = context;
         mCallback = callback;
+        mMaxBitmapSizePx = mContext.getResources().getInteger(
+                com.android.car.media.common.R.integer.media_items_bitmap_max_size_px);
     }
 
     /** Counter so callbacks from obsolete connections can be ignored. */
@@ -139,6 +145,8 @@
     @NonNull
     protected MediaBrowserCompat createMediaBrowser(@NonNull ComponentName browseService,
             @NonNull MediaBrowserCompat.ConnectionCallback callback) {
-        return new MediaBrowserCompat(mContext, browseService, callback, null);
+        Bundle rootHints = new Bundle();
+        rootHints.putInt(MediaConstants.EXTRA_MEDIA_ART_SIZE_HINT_PIXELS, mMaxBitmapSizePx);
+        return new MediaBrowserCompat(mContext, browseService, callback, rootHints);
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSource.java b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
index 0fd881c..f856eb2 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSource.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
@@ -31,185 +31,168 @@
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.service.media.MediaBrowserService;
+import android.text.TextUtils;
 import android.util.Log;
 
-import java.util.HashSet;
+import com.android.car.apps.common.BitmapUtils;
+import com.android.car.media.common.R;
+
+import java.net.URISyntaxException;
 import java.util.List;
 import java.util.Objects;
-import java.util.Set;
+
 
 /**
  * This represents a source of media content. It provides convenient methods to access media source
- * metadata, such as primary color and application name.
+ * metadata, such as application name and icon.
  */
 public class MediaSource {
     private static final String TAG = "MediaSource";
 
+    @NonNull
+    private final ComponentName mBrowseService;
+    @NonNull
+    private final CharSequence mDisplayName;
+    @NonNull
+    private final Drawable mIcon;
+
     /**
-     * Custom media sources which should not be templatized.
+     * Creates a {@link MediaSource} for the given {@link ComponentName}
      */
-    private static final Set<String> CUSTOM_MEDIA_SOURCES = new HashSet<>();
-    static {
-        CUSTOM_MEDIA_SOURCES.add("com.android.car.radio");
+    @Nullable
+    public static MediaSource create(@NonNull Context context,
+            @NonNull ComponentName componentName) {
+        ServiceInfo serviceInfo = getBrowseServiceInfo(context, componentName);
+
+        String className = serviceInfo != null ? serviceInfo.name : null;
+        if (TextUtils.isEmpty(className)) {
+            Log.w(TAG,
+                    "No MediaBrowserService found in component " + componentName.flattenToString());
+            return null;
+        }
+
+        try {
+            String packageName = componentName.getPackageName();
+            CharSequence displayName = extractDisplayName(context, serviceInfo, packageName);
+            Drawable icon = extractIcon(context, serviceInfo, packageName);
+            ComponentName browseService = new ComponentName(packageName, className);
+            return new MediaSource(browseService, displayName, icon);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Component not found " + componentName.flattenToString());
+            return null;
+        }
     }
 
-    private final String mPackageName;
-    @Nullable
-    private final String mBrowseServiceClassName;
-    private final Context mContext;
-    private CharSequence mName;
-
-    /**
-     * Creates a {@link MediaSource} for the given application package name
-     */
-    public MediaSource(@NonNull Context context, @NonNull String packageName) {
-        mContext = context;
-        mPackageName = packageName;
-        mBrowseServiceClassName = getBrowseServiceClassName(packageName);
-        extractComponentInfo(mPackageName, mBrowseServiceClassName);
+    private MediaSource(@NonNull ComponentName browseService, @NonNull CharSequence displayName,
+            @NonNull Drawable icon) {
+        mBrowseService = browseService;
+        mDisplayName = displayName;
+        mIcon = icon;
     }
 
     /**
-     * @return the classname corresponding to a {@link MediaBrowserService} in the media source, or
-     * null if the media source doesn't implement {@link MediaBrowserService}. A non-null result
-     * doesn't imply that this service is accessible. The consumer code should attempt to connect
-     * and handle rejections gracefully.
+     * @return the {@link ServiceInfo} corresponding to a {@link MediaBrowserService} in the media
+     * source, or null if the media source doesn't implement {@link MediaBrowserService}. A non-null
+     * result doesn't imply that this service is accessible. The consumer code should attempt to
+     * connect and handle rejections gracefully.
      */
     @Nullable
-    private String getBrowseServiceClassName(@NonNull String packageName) {
-        PackageManager packageManager = mContext.getPackageManager();
+    private static ServiceInfo getBrowseServiceInfo(@NonNull Context context,
+            @NonNull ComponentName componentName) {
+        PackageManager packageManager = context.getPackageManager();
         Intent intent = new Intent();
         intent.setAction(MediaBrowserService.SERVICE_INTERFACE);
-        intent.setPackage(packageName);
+        intent.setPackage(componentName.getPackageName());
         List<ResolveInfo> resolveInfos = packageManager.queryIntentServices(intent,
                 PackageManager.GET_RESOLVED_FILTER);
         if (resolveInfos == null || resolveInfos.isEmpty()) {
             return null;
         }
-        return resolveInfos.get(0).serviceInfo.name;
-    }
-
-
-
-    private void extractComponentInfo(@NonNull String packageName,
-            @Nullable String browseServiceClassName) {
-        try {
-            ApplicationInfo applicationInfo =
-                    mContext.getPackageManager().getApplicationInfo(packageName,
-                            PackageManager.GET_META_DATA);
-            ServiceInfo serviceInfo = browseServiceClassName != null
-                    ? mContext.getPackageManager().getServiceInfo(
-                    new ComponentName(packageName, browseServiceClassName),
-                    PackageManager.GET_META_DATA)
-                    : null;
-
-            // Get the proper app name, check service label, then application label.
-            if (serviceInfo != null && serviceInfo.labelRes != 0) {
-                mName = serviceInfo.loadLabel(mContext.getPackageManager());
-            } else {
-                mName = applicationInfo.loadLabel(mContext.getPackageManager());
+        String className = componentName.getClassName();
+        if (TextUtils.isEmpty(className)) {
+            return resolveInfos.get(0).serviceInfo;
+        }
+        for (ResolveInfo resolveInfo : resolveInfos) {
+            ServiceInfo result = resolveInfo.serviceInfo;
+            if (result.name.equals(className)) {
+                return result;
             }
-        } catch (PackageManager.NameNotFoundException e) {
-            Log.w(TAG, "Unable to update media client package attributes.", e);
         }
+        return null;
     }
 
     /**
-     * @return media source human readable name.
+     * @return a proper app name. Checks service label first. If failed, uses application label
+     * as fallback.
      */
-    public CharSequence getName() {
-        return mName;
+    @NonNull
+    private static CharSequence extractDisplayName(@NonNull Context context,
+            @Nullable ServiceInfo serviceInfo, @NonNull String packageName)
+            throws PackageManager.NameNotFoundException {
+        if (serviceInfo != null && serviceInfo.labelRes != 0) {
+            return serviceInfo.loadLabel(context.getPackageManager());
+        }
+        ApplicationInfo applicationInfo =
+                context.getPackageManager().getApplicationInfo(packageName,
+                        PackageManager.GET_META_DATA);
+        return applicationInfo.loadLabel(context.getPackageManager());
     }
 
     /**
-     * @return the package name that identifies this media source.
+     * @return a proper icon. Checks service icon first. If failed, uses application icon as
+     * fallback.
      */
+    @NonNull
+    private static Drawable extractIcon(@NonNull Context context, @Nullable ServiceInfo serviceInfo,
+            @NonNull String packageName) throws PackageManager.NameNotFoundException {
+        Drawable appIcon = serviceInfo != null ? serviceInfo.loadIcon(context.getPackageManager())
+                : context.getPackageManager().getApplicationIcon(packageName);
+
+        return BitmapUtils.maybeFlagDrawable(context, appIcon);
+    }
+
+    /**
+     * @return media source human readable name for display.
+     */
+    @NonNull
+    public CharSequence getDisplayName() {
+        return mDisplayName;
+    }
+
+    /**
+     * @return the package name of this media source.
+     */
+    @NonNull
     public String getPackageName() {
-        return mPackageName;
+        return mBrowseService.getPackageName();
     }
 
     /**
-     * @return a {@link ComponentName} referencing this media source's {@link MediaBrowserService},
-     * or NULL if this media source doesn't implement such service.
+     * @return a {@link ComponentName} referencing this media source's {@link MediaBrowserService}.
      */
-    @Nullable
+    @NonNull
     public ComponentName getBrowseServiceComponentName() {
-        if (mBrowseServiceClassName != null) {
-            return new ComponentName(mPackageName, mBrowseServiceClassName);
-        } else {
-            return null;
-        }
+        return mBrowseService;
     }
 
     /**
-     * Returns this media source's icon as a {@link Drawable}
+     * @return a {@link Drawable} as the media source's icon.
      */
-    public Drawable getPackageIcon() {
-        try {
-            return mContext.getPackageManager().getApplicationIcon(getPackageName());
-        } catch (PackageManager.NameNotFoundException e) {
-            return null;
-        }
+    @NonNull
+    public Drawable getIcon() {
+        return mIcon;
     }
 
     /**
      * Returns this media source's icon cropped to a circle.
      */
     public Bitmap getRoundPackageIcon() {
-        Drawable packageIcon = getPackageIcon();
-        return packageIcon != null
-                ? getRoundCroppedBitmap(drawableToBitmap(getPackageIcon()))
-                : null;
+        return getRoundCroppedBitmap(BitmapUtils.fromDrawable(mIcon, null));
     }
 
-    /**
-     * Returns {@code true} iff this media source should not be templatized.
-     */
-    public boolean isCustom() {
-        return isCustom(mPackageName);
-    }
-
-    /**
-     * Returns {@code true} iff the provided media package should not be templatized.
-     */
-    public static boolean isCustom(String packageName) {
-        return CUSTOM_MEDIA_SOURCES.contains(packageName);
-    }
-
-    /**
-     * Returns {@code true} iff this media source has a browse service to connect to.
-     */
-    public boolean isBrowsable() {
-        return mBrowseServiceClassName != null;
-    }
-
-    private Bitmap drawableToBitmap(Drawable drawable) {
-        Bitmap bitmap = null;
-
-        if (drawable instanceof BitmapDrawable) {
-            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
-            if (bitmapDrawable.getBitmap() != null) {
-                return bitmapDrawable.getBitmap();
-            }
-        }
-
-        if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
-            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
-        } else {
-            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
-                    drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
-        }
-
-        Canvas canvas = new Canvas(bitmap);
-        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
-        drawable.draw(canvas);
-        return bitmap;
-    }
-
-    private Bitmap getRoundCroppedBitmap(Bitmap bitmap) {
+    private static Bitmap getRoundCroppedBitmap(Bitmap bitmap) {
         Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
                 Bitmap.Config.ARGB_8888);
         Canvas canvas = new Canvas(output);
@@ -233,24 +216,36 @@
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         MediaSource that = (MediaSource) o;
-        return Objects.equals(mPackageName, that.mPackageName)
-                && Objects.equals(mBrowseServiceClassName, that.mBrowseServiceClassName);
+        return Objects.equals(mBrowseService, that.mBrowseService);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mPackageName, mBrowseServiceClassName);
+        return Objects.hash(mBrowseService);
     }
 
     @Override
     @NonNull
     public String toString() {
-        return getPackageName();
+        return mBrowseService.flattenToString();
     }
 
-    /** Returns the package name of the given source, or null. */
+    /**
+     * @return an intent to open the media source selector, or null if no source selector is
+     * configured.
+     * @param popup Whether the intent should point to the regular app selector (false), which
+     *              would open the selected media source in Media Center, or the "popup" version
+     *              (true), which would just select the source and dismiss itself.
+     */
     @Nullable
-    public static String getPackageName(@Nullable MediaSource source) {
-        return (source != null) ? source.getPackageName() : null;
+    public static Intent getSourceSelectorIntent(Context context, boolean popup) {
+        String uri = context.getString(popup ? R.string.launcher_popup_intent
+                : R.string.launcher_intent);
+        try {
+            return uri != null && !uri.isEmpty() ? Intent.parseUri(uri, Intent.URI_INTENT_SCHEME)
+                    : null;
+        } catch (URISyntaxException e) {
+            throw new IllegalStateException("Wrong app-launcher intent: " + uri, e);
+        }
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
index 2f85537..1d02209 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
@@ -26,7 +26,6 @@
 import android.car.CarNotConnectedException;
 import android.car.media.CarMediaManager;
 import android.content.ComponentName;
-import android.content.ContentValues;
 import android.media.session.MediaController;
 import android.os.Handler;
 import android.os.RemoteException;
@@ -40,8 +39,6 @@
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 
-import com.android.car.media.common.MediaConstants;
-
 import java.util.Objects;
 
 /**
@@ -79,7 +76,7 @@
 
         CarMediaManager getCarMediaManager(Car carApi) throws CarNotConnectedException;
 
-        MediaSource getMediaSource(String packageName);
+        MediaSource getMediaSource(ComponentName componentName);
     }
 
     /** Returns the MediaSourceViewModel singleton tied to the application. */
@@ -127,8 +124,9 @@
             }
 
             @Override
-            public MediaSource getMediaSource(String packageName) {
-                return packageName == null ? null : new MediaSource(application, packageName);
+            public MediaSource getMediaSource(ComponentName componentName) {
+                return componentName == null ? null : MediaSource.create(application,
+                        componentName);
             }
         });
     }
@@ -149,7 +147,7 @@
             if (browser != null) {
                 if (!browser.isConnected()) {
                     Log.e(TAG, "Browser is NOT connected !! "
-                            + mPrimaryMediaSource.getValue().getPackageName() + idHash(browser));
+                            + mPrimaryMediaSource.getValue().toString() + idHash(browser));
                     mMediaController.setValue(null);
                 } else {
                     mMediaController.setValue(mInputFactory.getControllerForSession(
@@ -163,8 +161,8 @@
                 mConnectedBrowserCallback);
 
         mHandler = new Handler(application.getMainLooper());
-        mMediaSourceListener = packageName -> mHandler.post(
-                () -> updateModelState(mInputFactory.getMediaSource(packageName)));
+        mMediaSourceListener = componentName -> mHandler.post(
+                () -> updateModelState(mInputFactory.getMediaSource(componentName)));
 
         try {
             mCarMediaManager = mInputFactory.getCarMediaManager(mCar);
@@ -188,13 +186,10 @@
     }
 
     /**
-     * Updates the primary media source, and notifies content provider of new source
+     * Updates the primary media source.
      */
-    public void setPrimaryMediaSource(MediaSource mediaSource) {
-        ContentValues values = new ContentValues();
-        values.put(MediaConstants.KEY_PACKAGE_NAME, mediaSource.getPackageName());
-
-        mCarMediaManager.setMediaSource(mediaSource.getPackageName());
+    public void setPrimaryMediaSource(@NonNull MediaSource mediaSource) {
+        mCarMediaManager.setMediaSource(mediaSource.getBrowseServiceComponentName());
     }
 
     /**
@@ -222,23 +217,13 @@
             return;
         }
 
-        // Reset dependent values to avoid propagating inconsistencies.
-        mMediaController.setValue(null);
-        mConnectedMediaBrowser.setValue(null);
-        mBrowserConnector.connectTo(null);
-
         // Broadcast the new source
         mPrimaryMediaSource.setValue(newMediaSource);
 
         // Recompute dependent values
-        if (newMediaSource == null) {
-            return;
+        if (newMediaSource != null) {
+            ComponentName browseService = newMediaSource.getBrowseServiceComponentName();
+            mBrowserConnector.connectTo(browseService);
         }
-
-        ComponentName browseService = newMediaSource.getBrowseServiceComponentName();
-        if (browseService == null) {
-            Log.e(TAG, "No browseService for source: " + newMediaSource.getPackageName());
-        }
-        mBrowserConnector.connectTo(browseService);
     }
 }
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java b/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
index 315c77c..7d423c8 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -29,7 +30,12 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.car.media.common.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
@@ -85,50 +91,72 @@
         mAppContext.registerReceiver(mAppInstallUninstallReceiver, filter);
     }
 
-    /** Returns the alphabetically sorted list of available media sources. */
+    /**
+     * Returns the sorted list of available media sources. Sources listed in the array resource
+     * R.array.preferred_media_sources are included first. Other sources follow in alphabetical
+     * order.
+     */
     public List<MediaSource> getList() {
         if (mMediaSources == null) {
-            mMediaSources = getPackageNames().stream()
+            // Get the flattened components to display first.
+            String[] preferredFlats = mAppContext.getResources().getStringArray(
+                    R.array.preferred_media_sources);
+
+            // Make a map of components to display first (the value is the component's index).
+            HashMap<ComponentName, Integer> preferredComps = new HashMap<>(preferredFlats.length);
+            for (int i = 0; i < preferredFlats.length; i++) {
+                preferredComps.put(ComponentName.unflattenFromString(preferredFlats[i]), i);
+            }
+
+            // Prepare an array of the sources to display first (unavailable preferred components
+            // will be excluded).
+            MediaSource[] preferredSources = new MediaSource[preferredFlats.length];
+            List<MediaSource> sortedSources = getComponentNames().stream()
                     .filter(Objects::nonNull)
-                    .map(packageName -> new MediaSource(mAppContext, packageName))
+                    .map(componentName -> MediaSource.create(mAppContext, componentName))
                     .filter(mediaSource -> {
-                        if (mediaSource.getName() == null) {
-                            Log.w(TAG, "Found media source without name: "
-                                    + mediaSource.getPackageName());
+                        if (mediaSource == null) {
+                            Log.w(TAG, "Media source is null");
+                            return false;
+                        }
+                        ComponentName srcComp = mediaSource.getBrowseServiceComponentName();
+                        if (preferredComps.containsKey(srcComp)) {
+                            // Record the source in the preferred array...
+                            preferredSources[preferredComps.get(srcComp)] = mediaSource;
+                            // And exclude it from the alpha sort.
                             return false;
                         }
                         return true;
                     })
-                    .sorted(Comparator.comparing(mediaSource -> mediaSource.getName().toString()))
+                    .sorted(Comparator.comparing(
+                            mediaSource -> mediaSource.getDisplayName().toString()))
                     .collect(Collectors.toList());
+
+            // Concatenate the non null preferred sources and the sorted ones into the result.
+            mMediaSources = new ArrayList<>(sortedSources.size() + preferredFlats.length);
+            Arrays.stream(preferredSources).filter(Objects::nonNull).forEach(mMediaSources::add);
+            mMediaSources.addAll(sortedSources);
         }
         return mMediaSources;
     }
 
     /**
-     * Generates a set of all possible apps to choose from, including the ones that are just
-     * media services.
+     * Generates a set of all possible media services to choose from.
      */
-    private Set<String> getPackageNames() {
+    private Set<ComponentName> getComponentNames() {
         PackageManager packageManager = mAppContext.getPackageManager();
-        Intent intent = new Intent(Intent.ACTION_MAIN, null);
-        intent.addCategory(Intent.CATEGORY_APP_MUSIC);
-
         Intent mediaIntent = new Intent();
         mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
-
-        List<ResolveInfo> availableActivities = packageManager.queryIntentActivities(intent, 0);
         List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent,
                 PackageManager.GET_RESOLVED_FILTER);
 
-        Set<String> apps = new HashSet<>();
+        Set<ComponentName> components = new HashSet<>();
         for (ResolveInfo info : mediaServices) {
-            apps.add(info.serviceInfo.packageName);
+            ComponentName componentName = new ComponentName(info.serviceInfo.packageName,
+                    info.serviceInfo.name);
+            components.add(componentName);
         }
-        for (ResolveInfo info : availableActivities) {
-            apps.add(info.activityInfo.packageName);
-        }
-        return apps;
+        return components;
     }
 
 }
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
index 3cc4cc3..b825fa1 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
@@ -122,7 +122,7 @@
             }
 
             @Override
-            public MediaSource getMediaSource(String packageName) {
+            public MediaSource getMediaSource(ComponentName componentName) {
                 return mMediaSource;
             }
         });
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourcesLiveDataTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourcesLiveDataTest.java
index 431cab5..d9d4e82 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourcesLiveDataTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourcesLiveDataTest.java
@@ -111,8 +111,7 @@
         assertThat(
                 observedValue.stream().map(source -> source.getPackageName())
                         .collect(Collectors.toList()))
-                .containsExactly(TEST_ACTIVITY_PACKAGE_1, TEST_SERVICE_PACKAGE_1,
-                        TEST_SERVICE_PACKAGE_WITH_METADATA);
+                .containsExactly(TEST_SERVICE_PACKAGE_1, TEST_SERVICE_PACKAGE_WITH_METADATA);
     }
 
     @Test
@@ -135,8 +134,7 @@
         assertThat(
                 observedValue.stream().map(source -> source.getPackageName())
                         .collect(Collectors.toList()))
-                .containsExactly(TEST_ACTIVITY_PACKAGE_1, TEST_ACTIVITY_PACKAGE_2,
-                        TEST_SERVICE_PACKAGE_1, TEST_SERVICE_PACKAGE_2,
+                .containsExactly(TEST_SERVICE_PACKAGE_1, TEST_SERVICE_PACKAGE_2,
                         TEST_SERVICE_PACKAGE_WITH_METADATA);
     }
 
@@ -159,7 +157,7 @@
         assertThat(
                 observedValue.stream().map(source -> source.getPackageName())
                         .collect(Collectors.toList()))
-                .containsExactly(TEST_ACTIVITY_PACKAGE_1, TEST_SERVICE_PACKAGE_WITH_METADATA);
+                .containsExactly(TEST_SERVICE_PACKAGE_WITH_METADATA);
     }
 
     @NonNull
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-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-messenger-common/AndroidManifest.xml
similarity index 60%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-messenger-common/AndroidManifest.xml
index c5d298b..148737d 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-messenger-common/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright 2019 The Android Open Source Project
+  ~ 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.
@@ -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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+
+<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-messenger-common/res/values/dimens.xml b/car-messenger-common/res/values/dimens.xml
new file mode 100644
index 0000000..ea87725
--- /dev/null
+++ b/car-messenger-common/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?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>
+    <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/res/values/strings.xml b/car-telephony-common/res/values/strings.xml
index e4f7b52..24a3300 100644
--- a/car-telephony-common/res/values/strings.xml
+++ b/car-telephony-common/res/values/strings.xml
@@ -42,4 +42,9 @@
     <!-- Status label for phone state. &#8230; is an ellipsis. [CHAR LIMIT=25] -->
     <string name="call_state_call_ending">Disconnecting&#8230;</string>
 
+    <!-- String format used to format a address Uri. -->
+    <string name="address_uri_format" translatable="false">geo:0,0?q=%s</string>
+    <!-- String format used to format a navigation Uri. -->
+    <string name="navigation_uri_format" translatable="false">https://maps.google.com/maps?daddr=%s&amp;nav=1</string>
+
 </resources>
\ No newline at end of file
diff --git a/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java b/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
index d2665a8..69f00d5 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/AsyncQueryLiveData.java
@@ -19,7 +19,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
-import android.os.HandlerThread;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
@@ -27,6 +26,9 @@
 import androidx.annotation.WorkerThread;
 import androidx.lifecycle.LiveData;
 
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
 /**
  * Asynchronously queries a {@link ContentResolver} for a given query and observes the loaded data
  * for changes, reloading if necessary.
@@ -36,19 +38,21 @@
 public abstract class AsyncQueryLiveData<T> extends LiveData<T> {
 
     private static final String TAG = "CD.AsyncQueryLiveData";
-    private static HandlerThread sHandlerThread;
-
-    static {
-        sHandlerThread = new HandlerThread(AsyncQueryLiveData.class.getName());
-        sHandlerThread.start();
-    }
+    private final ExecutorService mExecutorService;
 
     private final ObservableAsyncQuery mObservableAsyncQuery;
     private CursorRunnable mCurrentCursorRunnable;
+    private Future<?> mCurrentRunnableFuture;
 
     public AsyncQueryLiveData(Context context, QueryParam.Provider provider) {
+        this(context, provider, WorkerExecutor.getInstance().getSingleThreadExecutor());
+    }
+
+    public AsyncQueryLiveData(Context context, QueryParam.Provider provider,
+            ExecutorService executorService) {
         mObservableAsyncQuery = new ObservableAsyncQuery(provider, context.getContentResolver(),
                 this::onCursorLoaded);
+        mExecutorService = executorService;
     }
 
     @Override
@@ -62,6 +66,7 @@
         super.onInactive();
         if (mCurrentCursorRunnable != null) {
             mCurrentCursorRunnable.closeCursorIfNecessary();
+            mCurrentRunnableFuture.cancel(false);
         }
         mObservableAsyncQuery.stopQuery();
     }
@@ -76,9 +81,10 @@
         Log.d(TAG, "onCursorLoaded: " + this);
         if (mCurrentCursorRunnable != null) {
             mCurrentCursorRunnable.closeCursorIfNecessary();
+            mCurrentRunnableFuture.cancel(false);
         }
         mCurrentCursorRunnable = new CursorRunnable(cursor);
-        sHandlerThread.getThreadHandler().post(mCurrentCursorRunnable);
+        mCurrentRunnableFuture = mExecutorService.submit(mCurrentCursorRunnable);
     }
 
     private class CursorRunnable implements Runnable {
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 e95cb3e..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
@@ -23,7 +23,6 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.provider.ContactsContract;
-import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -40,27 +39,136 @@
     private static final String TAG = "CD.Contact";
 
     /**
+     * Column name for phonebook label column.
+     */
+    private static final String PHONEBOOK_LABEL = "phonebook_label";
+    /**
+     * Column name for alternative phonebook label column.
+     */
+    private static final String PHONEBOOK_LABEL_ALT = "phonebook_label_alt";
+
+    /**
      * Contact belongs to TYPE_LETTER if its display name starts with a letter
      */
     private static final int TYPE_LETTER = 1;
-
     /**
      * Contact belongs to TYPE_DIGIT if its display name starts with a digit
      */
     private static final int TYPE_DIGIT = 2;
-
     /**
-     * Contact belongs to TYPE_OTHER if it does not belong to TYPE_LETTER or TYPE_DIGIT
-     * Such as empty display name or the display name starts with "_"
+     * Contact belongs to TYPE_OTHER if it does not belong to TYPE_LETTER or TYPE_DIGIT Such as
+     * empty display name or the display name starts with "_"
      */
     private static final int TYPE_OTHER = 3;
 
+    /**
+     * A reference to the {@link ContactsContract.RawContacts#CONTACT_ID}.
+     */
+    private long mContactId;
 
     /**
-     * A reference to the {@link ContactsContract.Contacts#_ID} that this data belongs to. See
-     * {@link ContactsContract.Contacts.Entity#CONTACT_ID}
+     * A reference to the {@link ContactsContract.Data#RAW_CONTACT_ID}.
      */
-    private long mId;
+    private long mRawContactId;
+
+    /**
+     * The name of the account instance to which this row belongs, which identifies a specific
+     * account. See {@link ContactsContract.RawContacts#ACCOUNT_NAME}.
+     */
+    private String mAccountName;
+
+    /**
+     * The display name.
+     * <p>
+     * The standard text shown as the contact's display name, based on the best available
+     * information for the contact.
+     * </p>
+     * <p>
+     * See {@link ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME}.
+     */
+    private String mDisplayName;
+
+    /**
+     * The alternative display name.
+     * <p>
+     * An alternative representation of the display name, such as "family name first" instead of
+     * "given name first" for Western names.  If an alternative is not available, the values should
+     * be the same as {@link #mDisplayName}.
+     * </p>
+     * <p>
+     * See {@link ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME_ALTERNATIVE}.
+     */
+    private String mDisplayNameAlt;
+
+    /**
+     * The given name for the contact. See
+     * {@link ContactsContract.CommonDataKinds.StructuredName#GIVEN_NAME}.
+     */
+    private String mGivenName;
+
+    /**
+     * The family name for the contact. See
+     * {@link ContactsContract.CommonDataKinds.StructuredName#FAMILY_NAME}.
+     */
+    private String mFamilyName;
+
+    /**
+     * The initials of the contact's name.
+     */
+    private String mInitials;
+
+    /**
+     * The phonebook label.
+     * <p>
+     * For {@link #mDisplayName}s starting with letters, label will be the first character of {@link
+     * #mDisplayName}. For {@link #mDisplayName}s starting with numbers, the label will be "#". For
+     * {@link #mDisplayName}s starting with other characters, the label will be "...".
+     * </p>
+     */
+    private String mPhoneBookLabel;
+
+    /**
+     * The alternative phonebook label.
+     * <p>
+     * It is similar with {@link #mPhoneBookLabel}. But instead of generating from {@link
+     * #mDisplayName}, it will use {@link #mDisplayNameAlt}.
+     * </p>
+     */
+    private String mPhoneBookLabelAlt;
+
+    /**
+     * Sort key that takes into account locale-based traditions for sorting names in address books.
+     * <p>
+     * See {@link ContactsContract.CommonDataKinds.Phone#SORT_KEY_PRIMARY}.
+     */
+    private String mSortKeyPrimary;
+
+    /**
+     * Sort key based on the alternative representation of the full name.
+     * <p>
+     * See {@link ContactsContract.CommonDataKinds.Phone#SORT_KEY_ALTERNATIVE}.
+     */
+    private String mSortKeyAlt;
+
+    /**
+     * An opaque value that contains hints on how to find the contact if its row id changed as a
+     * result of a sync or aggregation. If a contact has multiple phone numbers, all phone numbers
+     * are recorded in a single entry and they all have the same look up key in a single load. See
+     * {@link ContactsContract.Data#LOOKUP_KEY}.
+     */
+    private String mLookupKey;
+
+    /**
+     * A URI that can be used to retrieve a thumbnail of the contact's photo.
+     */
+    @Nullable
+    private Uri mAvatarThumbnailUri;
+
+    /**
+     * A URI that can be used to retrieve the contact's full-size photo.
+     */
+    @Nullable
+    private Uri mAvatarUri;
 
     /**
      * Whether this contact entry is starred by user.
@@ -68,118 +176,195 @@
     private boolean mIsStarred;
 
     /**
-     * Contact-specific information about whether or not a contact has been pinned by the user at
-     * a particular position within the system contact application's user interface.
+     * Contact-specific information about whether or not a contact has been pinned by the user at a
+     * particular position within the system contact application's user interface.
      */
     private int mPinnedPosition;
 
     /**
-     * All phone numbers of this contact mapping to the unique primary key for the raw data entry.
+     * This contact's primary phone number. Its value is null if a primary phone number is not set.
      */
-    private List<PhoneNumber> mPhoneNumbers = new ArrayList<>();
-
-    /**
-     * The display name.
-     * <p>
-     * The standard text shown as the contact's display name, based on the best
-     * available information for the contact.
-     * </p>
-     *
-     * @see ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME
-     */
-    private String mDisplayName;
-
-    /**
-     * The alternative display name.
-     * <p>
-     * An alternative representation of the display name, such as "family name first"
-     * instead of "given name first" for Western names.  If an alternative is not
-     * available, the values should be the same as {@link #mDisplayName}.
-     * </p>
-     *
-     * @see ContactsContract.CommonDataKinds.Phone#DISPLAY_NAME_ALTERNATIVE
-     */
-    private String mAltDisplayName;
-
-    /**
-     * A URI that can be used to retrieve a thumbnail of the contact's photo.
-     */
-    private Uri mAvatarThumbnailUri;
-
-    /**
-     * A URI that can be used to retrieve the contact's full-size photo.
-     */
-    private Uri mAvatarUri;
-
-    /**
-     * An opaque value that contains hints on how to find the contact if its row id changed
-     * as a result of a sync or aggregation. If a contact has multiple phone numbers, all phone
-     * numbers are recorded in a single entry and they all have the same look up key in a single
-     * load.
-     */
-    private String mLookupKey;
+    @Nullable
+    private PhoneNumber mPrimaryPhoneNumber;
 
     /**
      * Whether this contact represents a voice mail.
      */
     private boolean mIsVoiceMail;
 
-    private PhoneNumber mPrimaryPhoneNumber;
+    /**
+     * All phone numbers of this contact mapping to the unique primary key for the raw data entry.
+     */
+    private final List<PhoneNumber> mPhoneNumbers = new ArrayList<>();
 
     /**
-     * Parses a Contact entry for a Cursor loaded from the Contact Database.
+     * All postal addresses of this contact mapping to the unique primary key for the raw data
+     * entry
+     */
+    private final List<PostalAddress> mPostalAddresses = new ArrayList<>();
+
+    /**
+     * Parses a contact entry for a Cursor loaded from the Contact Database. A new contact will be
+     * created and returned.
      */
     public static Contact fromCursor(Context context, Cursor cursor) {
-        int contactIdColumn = cursor.getColumnIndex(
-                ContactsContract.CommonDataKinds.Phone.CONTACT_ID);
+        return fromCursor(context, cursor, null);
+    }
+
+    /**
+     * Parses a contact entry for a Cursor loaded from the Contact Database.
+     *
+     * @param contact should have the same {@link #mLookupKey} and {@link #mAccountName} with the
+     *                data read from the cursor, so all the data from the cursor can be loaded into
+     *                this contact. If either of their {@link #mLookupKey} and {@link #mAccountName}
+     *                is not the same or this contact is null, a new contact will be created and
+     *                returned.
+     */
+    public static Contact fromCursor(Context context, Cursor cursor, @Nullable Contact contact) {
+        int accountNameColumn = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME);
+        int lookupKeyColumn = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
+        String accountName = cursor.getString(accountNameColumn);
+        String lookupKey = cursor.getString(lookupKeyColumn);
+
+        if (contact == null) {
+            contact = new Contact();
+            contact.loadBasicInfo(cursor);
+        }
+
+        if (!TextUtils.equals(accountName, contact.mAccountName)
+                || !TextUtils.equals(lookupKey, contact.mLookupKey)) {
+            Log.w(TAG, "A wrong contact is passed in. A new contact will be created.");
+            contact = new Contact();
+            contact.loadBasicInfo(cursor);
+        }
+
+        int mimetypeColumn = cursor.getColumnIndex(ContactsContract.Data.MIMETYPE);
+        String mimeType = cursor.getString(mimetypeColumn);
+
+        // More mimeType can be added here if more types of data needs to be loaded.
+        switch (mimeType) {
+            case ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE:
+                contact.loadNameDetails(cursor);
+                break;
+            case ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE:
+                contact.addPhoneNumber(context, cursor);
+                break;
+            case ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE:
+                contact.addPostalAddress(cursor);
+                break;
+            default:
+                Log.d(TAG,
+                        String.format("This mimetype %s will not be loaded right now.", mimeType));
+        }
+
+        return contact;
+    }
+
+    /**
+     * The data columns that are the same in every cursor no matter what the mimetype is will be
+     * loaded here.
+     */
+    private void loadBasicInfo(Cursor cursor) {
+        int contactIdColumn = cursor.getColumnIndex(ContactsContract.RawContacts.CONTACT_ID);
+        int rawContactIdColumn = cursor.getColumnIndex(ContactsContract.Data.RAW_CONTACT_ID);
+        int accountNameColumn = cursor.getColumnIndex(ContactsContract.RawContacts.ACCOUNT_NAME);
+        int displayNameColumn = cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME);
+        int displayNameAltColumn = cursor.getColumnIndex(
+                ContactsContract.RawContacts.DISPLAY_NAME_ALTERNATIVE);
+        int phoneBookLabelColumn = cursor.getColumnIndex(PHONEBOOK_LABEL);
+        int phoneBookLabelAltColumn = cursor.getColumnIndex(PHONEBOOK_LABEL_ALT);
+        int sortKeyPrimaryColumn = cursor.getColumnIndex(
+                ContactsContract.RawContacts.SORT_KEY_PRIMARY);
+        int sortKeyAltColumn = cursor.getColumnIndex(
+                ContactsContract.RawContacts.SORT_KEY_ALTERNATIVE);
+        int lookupKeyColumn = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
+
+        int avatarUriColumn = cursor.getColumnIndex(ContactsContract.Data.PHOTO_URI);
+        int avatarThumbnailColumn = cursor.getColumnIndex(
+                ContactsContract.Data.PHOTO_THUMBNAIL_URI);
         int starredColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.STARRED);
         int pinnedColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PINNED);
-        int displayNameColumn = cursor.getColumnIndex(
-                ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
-        int altDisplayNameColumn = cursor.getColumnIndex(
-                ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME_ALTERNATIVE);
-        int avatarUriColumn = cursor.getColumnIndex(
-                ContactsContract.CommonDataKinds.Phone.PHOTO_URI);
-        int avatarThumbnailColumn = cursor.getColumnIndex(
-                ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI);
-        int lookupKeyColumn = cursor.getColumnIndex(
-                ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY);
 
-        Contact contact = new Contact();
-        contact.mId = cursor.getLong(contactIdColumn);
-        contact.mDisplayName = cursor.getString(displayNameColumn);
-        contact.mAltDisplayName = cursor.getString(altDisplayNameColumn);
-
-        PhoneNumber number = PhoneNumber.fromCursor(context, cursor);
-        contact.mPhoneNumbers.add(number);
-        if (number.isPrimary()) {
-            contact.mPrimaryPhoneNumber = number;
-        }
-
-        contact.mIsStarred = cursor.getInt(starredColumn) > 0;
-        contact.mPinnedPosition = cursor.getInt(pinnedColumn);
-        contact.mIsVoiceMail = TelecomUtils.isVoicemailNumber(context, number.getNumber());
+        mContactId = cursor.getLong(contactIdColumn);
+        mRawContactId = cursor.getLong(rawContactIdColumn);
+        mAccountName = cursor.getString(accountNameColumn);
+        mDisplayName = cursor.getString(displayNameColumn);
+        mDisplayNameAlt = cursor.getString(displayNameAltColumn);
+        mSortKeyPrimary = cursor.getString(sortKeyPrimaryColumn);
+        mSortKeyAlt = cursor.getString(sortKeyAltColumn);
+        mPhoneBookLabel = cursor.getString(phoneBookLabelColumn);
+        mPhoneBookLabelAlt = cursor.getString(phoneBookLabelAltColumn);
+        mLookupKey = cursor.getString(lookupKeyColumn);
 
         String avatarUriStr = cursor.getString(avatarUriColumn);
-        contact.mAvatarUri = avatarUriStr == null ? null : Uri.parse(avatarUriStr);
-
+        mAvatarUri = avatarUriStr == null ? null : Uri.parse(avatarUriStr);
         String avatarThumbnailStringUri = cursor.getString(avatarThumbnailColumn);
-        contact.mAvatarThumbnailUri = avatarThumbnailStringUri == null ? null : Uri.parse(
+        mAvatarThumbnailUri = avatarThumbnailStringUri == null ? null : Uri.parse(
                 avatarThumbnailStringUri);
 
-        String lookUpKey = cursor.getString(lookupKeyColumn);
-        if (lookUpKey != null) {
-            contact.mLookupKey = lookUpKey;
-        } else {
-            Log.w(TAG, "Look up key is null. Fallback to use display name");
-            contact.mLookupKey = contact.mDisplayName;
+        mIsStarred = cursor.getInt(starredColumn) > 0;
+        mPinnedPosition = cursor.getInt(pinnedColumn);
+    }
+
+    /**
+     * Loads the data whose mimetype is
+     * {@link ContactsContract.CommonDataKinds.StructuredName#CONTENT_ITEM_TYPE}.
+     */
+    private void loadNameDetails(Cursor cursor) {
+        int firstNameColumn = cursor.getColumnIndex(
+                ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME);
+        int lastNameColumn = cursor.getColumnIndex(
+                ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME);
+
+        mGivenName = cursor.getString(firstNameColumn);
+        mFamilyName = cursor.getString(lastNameColumn);
+    }
+
+    /**
+     * Loads the data whose mimetype is
+     * {@link ContactsContract.CommonDataKinds.Phone#CONTENT_ITEM_TYPE}.
+     */
+    private void addPhoneNumber(Context context, Cursor cursor) {
+        PhoneNumber newNumber = PhoneNumber.fromCursor(context, cursor);
+
+        boolean hasSameNumber = false;
+        for (PhoneNumber number : mPhoneNumbers) {
+            if (newNumber.equals(number)) {
+                hasSameNumber = true;
+                number.merge(newNumber);
+            }
         }
-        return contact;
+
+        if (!hasSameNumber) {
+            mPhoneNumbers.add(newNumber);
+        }
+
+        if (newNumber.isPrimary()) {
+            mPrimaryPhoneNumber = newNumber.merge(mPrimaryPhoneNumber);
+        }
+
+        // TODO: update voice mail number part when start to support voice mail.
+        if (TelecomUtils.isVoicemailNumber(context, newNumber.getNumber())) {
+            mIsVoiceMail = true;
+        }
+    }
+
+    /**
+     * Loads the data whose mimetype is
+     * {@link ContactsContract.CommonDataKinds.StructuredPostal#CONTENT_ITEM_TYPE}.
+     */
+    private void addPostalAddress(Cursor cursor) {
+        PostalAddress newAddress = PostalAddress.fromCursor(cursor);
+
+        if (!mPostalAddresses.contains(newAddress)) {
+            mPostalAddresses.add(newAddress);
+        }
     }
 
     @Override
     public boolean equals(Object obj) {
-        return obj instanceof Contact && mLookupKey.equals(((Contact) obj).mLookupKey);
+        return obj instanceof Contact && mLookupKey.equals(((Contact) obj).mLookupKey)
+                && mAccountName.equals(((Contact) obj).mAccountName);
     }
 
     @Override
@@ -192,98 +377,173 @@
         return mDisplayName + mPhoneNumbers;
     }
 
+    /**
+     * Returns the aggregated contact id.
+     */
+    public long getId() {
+        return mContactId;
+    }
+
+    /**
+     * Returns the raw contact id.
+     */
+    public long getRawContactId() {
+        return mRawContactId;
+    }
+
+    /**
+     * Returns a lookup uri using {@link #mContactId} and {@link #mLookupKey}. Returns null if
+     * unable to get a valid lookup URI from the provided parameters. See {@link
+     * ContactsContract.Contacts#getLookupUri(long, String)}.
+     */
+    @Nullable
+    public Uri getLookupUri() {
+        return ContactsContract.Contacts.getLookupUri(mContactId, mLookupKey);
+    }
+
+    /**
+     * Returns {@link #mAccountName}.
+     */
+    public String getAccountName() {
+        return mAccountName;
+    }
+
+    /**
+     * Returns {@link #mDisplayName}.
+     */
     public String getDisplayName() {
         return mDisplayName;
     }
 
     /**
-     * Returns alternative display name.
+     * Returns {@link #mDisplayNameAlt}.
      */
-    public String getAltDisplayName() {
-        return mAltDisplayName;
+    public String getDisplayNameAlt() {
+        return mDisplayNameAlt;
     }
 
-    public boolean isVoicemail() {
-        return mIsVoiceMail;
+    /**
+     * Returns {@link #mGivenName}.
+     */
+    public String getGivenName() {
+        return mGivenName;
     }
 
-    @Nullable
-    public Uri getAvatarUri() {
-        return mAvatarThumbnailUri != null ? mAvatarThumbnailUri : mAvatarUri;
+    /**
+     * Returns {@link #mFamilyName}.
+     */
+    public String getFamilyName() {
+        return mFamilyName;
     }
 
+    /**
+     * Returns the initials of the contact's name.
+     */
+    //TODO: update how to get initials after refactoring. Could use last name and first name to
+    // get initials after refactoring to avoid error for those names with prefix.
+    public String getInitials() {
+        if (mInitials == null) {
+            mInitials = TelecomUtils.getInitials(mDisplayName, mDisplayNameAlt);
+        }
+
+        return mInitials;
+    }
+
+    /**
+     * Returns {@link #mPhoneBookLabel}
+     */
+    public String getPhonebookLabel() {
+        return mPhoneBookLabel;
+    }
+
+    /**
+     * Returns {@link #mPhoneBookLabelAlt}
+     */
+    public String getPhonebookLabelAlt() {
+        return mPhoneBookLabelAlt;
+    }
+
+    /**
+     * Returns {@link #mLookupKey}.
+     */
     public String getLookupKey() {
         return mLookupKey;
     }
 
-    public Uri getLookupUri() {
-        return ContactsContract.Contacts.getLookupUri(mId, mLookupKey);
+    /**
+     * Returns the Uri for avatar.
+     */
+    @Nullable
+    public Uri getAvatarUri() {
+        return mAvatarUri != null ? mAvatarUri : mAvatarThumbnailUri;
     }
 
-    /** Return all phone numbers associated with this contact. */
+    /**
+     * Return all phone numbers associated with this contact.
+     */
     public List<PhoneNumber> getNumbers() {
         return mPhoneNumbers;
     }
 
-    /** Return the aggregated contact id. */
-    public long getId() {
-        return mId;
+    /**
+     * Return all postal addresses associated with this contact.
+     */
+    public List<PostalAddress> getPostalAddresses() {
+        return mPostalAddresses;
     }
 
+    /**
+     * Returns if this Contact represents a voice mail number.
+     */
+    public boolean isVoicemail() {
+        return mIsVoiceMail;
+    }
+
+    /**
+     * Returns if this contact has a primary phone number.
+     */
+    public boolean hasPrimaryPhoneNumber() {
+        return mPrimaryPhoneNumber != null;
+    }
+
+    /**
+     * Returns the primary phone number for this Contact. Returns null if there is not one.
+     */
+    @Nullable
+    public PhoneNumber getPrimaryPhoneNumber() {
+        return mPrimaryPhoneNumber;
+    }
+
+    /**
+     * Returns if this Contact is starred.
+     */
     public boolean isStarred() {
         return mIsStarred;
     }
 
+    /**
+     * Returns {@link #mPinnedPosition}.
+     */
     public int getPinnedPosition() {
         return mPinnedPosition;
     }
 
     /**
-     * Merges a Contact entry with another if they represent different numbers of the same contact.
-     *
-     * @return A merged contact.
-     */
-    public Contact merge(Contact contact) {
-        if (equals(contact)) {
-            for (PhoneNumber phoneNumber : contact.mPhoneNumbers) {
-                int indexOfPhoneNumber = mPhoneNumbers.indexOf(phoneNumber);
-                if (indexOfPhoneNumber < 0) {
-                    mPhoneNumbers.add(phoneNumber);
-                } else {
-                    PhoneNumber existingPhoneNumber = mPhoneNumbers.get(indexOfPhoneNumber);
-                    existingPhoneNumber.merge(phoneNumber);
-                }
-            }
-            if (contact.mPrimaryPhoneNumber != null) {
-                mPrimaryPhoneNumber = contact.mPrimaryPhoneNumber.merge(mPrimaryPhoneNumber);
-            }
-        }
-        return this;
-    }
-
-    /**
-     * Looks up a {@link PhoneNumber} of this contact for the given phone number. Returns {@code
-     * null} if this contact doesn't contain the given phone number.
+     * Looks up a {@link PhoneNumber} of this contact for the given phone number string. Returns
+     * {@code null} if this contact doesn't contain the given phone number.
      */
     @Nullable
-    public PhoneNumber getPhoneNumber(String number) {
+    public PhoneNumber getPhoneNumber(Context context, String number) {
+        I18nPhoneNumberWrapper i18nPhoneNumber = I18nPhoneNumberWrapper.Factory.INSTANCE.get(
+                context, number);
         for (PhoneNumber phoneNumber : mPhoneNumbers) {
-            if (PhoneNumberUtils.compare(phoneNumber.getNumber(), number)) {
+            if (phoneNumber.getI18nPhoneNumberWrapper().equals(i18nPhoneNumber)) {
                 return phoneNumber;
             }
         }
         return null;
     }
 
-    public PhoneNumber getPrimaryPhoneNumber() {
-        return mPrimaryPhoneNumber;
-    }
-
-    /** Return if this contact has a primary phone number. */
-    public boolean hasPrimaryPhoneNumber() {
-        return mPrimaryPhoneNumber != null;
-    }
-
     @Override
     public int describeContents() {
         return 0;
@@ -291,19 +551,32 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeLong(mId);
+        dest.writeLong(mContactId);
+        dest.writeLong(mRawContactId);
+        dest.writeString(mLookupKey);
+        dest.writeString(mAccountName);
+        dest.writeString(mDisplayName);
+        dest.writeString(mDisplayNameAlt);
+        dest.writeString(mSortKeyPrimary);
+        dest.writeString(mSortKeyAlt);
+        dest.writeString(mPhoneBookLabel);
+        dest.writeString(mPhoneBookLabelAlt);
+        dest.writeParcelable(mAvatarThumbnailUri, 0);
+        dest.writeParcelable(mAvatarUri, 0);
         dest.writeBoolean(mIsStarred);
         dest.writeInt(mPinnedPosition);
+
+        dest.writeBoolean(mIsVoiceMail);
+        dest.writeParcelable(mPrimaryPhoneNumber, flags);
         dest.writeInt(mPhoneNumbers.size());
         for (PhoneNumber phoneNumber : mPhoneNumbers) {
             dest.writeParcelable(phoneNumber, flags);
         }
-        dest.writeString(mDisplayName);
-        dest.writeString(mAltDisplayName);
-        dest.writeParcelable(mAvatarThumbnailUri, 0);
-        dest.writeParcelable(mAvatarUri, 0);
-        dest.writeString(mLookupKey);
-        dest.writeBoolean(mIsVoiceMail);
+
+        dest.writeInt(mPostalAddresses.size());
+        for (PostalAddress postalAddress : mPostalAddresses) {
+            dest.writeParcelable(postalAddress, flags);
+        }
     }
 
     public static final Creator<Contact> CREATOR = new Creator<Contact>() {
@@ -318,14 +591,29 @@
         }
     };
 
-    /** Create {@link Contact} object from saved parcelable. */
+    /**
+     * Create {@link Contact} object from saved parcelable.
+     */
     private static Contact fromParcel(Parcel source) {
         Contact contact = new Contact();
-        contact.mId = source.readLong();
+        contact.mContactId = source.readLong();
+        contact.mRawContactId = source.readLong();
+        contact.mLookupKey = source.readString();
+        contact.mAccountName = source.readString();
+        contact.mDisplayName = source.readString();
+        contact.mDisplayNameAlt = source.readString();
+        contact.mSortKeyPrimary = source.readString();
+        contact.mSortKeyAlt = source.readString();
+        contact.mPhoneBookLabel = source.readString();
+        contact.mPhoneBookLabelAlt = source.readString();
+        contact.mAvatarThumbnailUri = source.readParcelable(Uri.class.getClassLoader());
+        contact.mAvatarUri = source.readParcelable(Uri.class.getClassLoader());
         contact.mIsStarred = source.readBoolean();
         contact.mPinnedPosition = source.readInt();
+
+        contact.mIsVoiceMail = source.readBoolean();
+        contact.mPrimaryPhoneNumber = source.readParcelable(PhoneNumber.class.getClassLoader());
         int phoneNumberListLength = source.readInt();
-        contact.mPhoneNumbers = new ArrayList<>();
         for (int i = 0; i < phoneNumberListLength; i++) {
             PhoneNumber phoneNumber = source.readParcelable(PhoneNumber.class.getClassLoader());
             contact.mPhoneNumbers.add(phoneNumber);
@@ -333,12 +621,13 @@
                 contact.mPrimaryPhoneNumber = phoneNumber;
             }
         }
-        contact.mDisplayName = source.readString();
-        contact.mAltDisplayName = source.readString();
-        contact.mAvatarThumbnailUri = source.readParcelable(Uri.class.getClassLoader());
-        contact.mAvatarUri = source.readParcelable(Uri.class.getClassLoader());
-        contact.mLookupKey = source.readString();
-        contact.mIsVoiceMail = source.readBoolean();
+
+        int postalAddressListLength = source.readInt();
+        for (int i = 0; i < postalAddressListLength; i++) {
+            PostalAddress address = source.readParcelable(PostalAddress.class.getClassLoader());
+            contact.mPostalAddresses.add(address);
+        }
+
         return contact;
     }
 
@@ -346,31 +635,33 @@
     public int compareTo(Contact otherContact) {
         // Use a helper function to classify Contacts
         // and by default, it should be compared by first name order.
-        return compareByDisplayName(otherContact);
+        return compareBySortKeyPrimary(otherContact);
     }
 
     /**
-     * Compares contacts by their {@link #mDisplayName} in an order of
-     * letters, numbers, then special characters.
+     * Compares contacts by their {@link #mSortKeyPrimary} in an order of letters, numbers, then
+     * special characters.
      */
-    public int compareByDisplayName(@NonNull Contact otherContact) {
-        return compareNames(mDisplayName, otherContact.getDisplayName());
+    public int compareBySortKeyPrimary(@NonNull Contact otherContact) {
+        return compareNames(mSortKeyPrimary, otherContact.mSortKeyPrimary,
+                mPhoneBookLabel, otherContact.getPhonebookLabel());
     }
 
     /**
-     * Compares contacts by their {@link #mAltDisplayName} in an order of
-     * letters, numbers, then special characters.
+     * Compares contacts by their {@link #mSortKeyAlt} in an order of letters, numbers, then special
+     * characters.
      */
-    public int compareByAltDisplayName(@NonNull Contact otherContact) {
-        return compareNames(mAltDisplayName, otherContact.getAltDisplayName());
+    public int compareBySortKeyAlt(@NonNull Contact otherContact) {
+        return compareNames(mSortKeyAlt, otherContact.mSortKeyAlt,
+                mPhoneBookLabelAlt, otherContact.getPhonebookLabelAlt());
     }
 
     /**
      * Compares two strings in an order of letters, numbers, then special characters.
      */
-    private int compareNames(String name, String otherName) {
-        int type = getNameType(name);
-        int otherType = getNameType(otherName);
+    private int compareNames(String name, String otherName, String label, String otherLabel) {
+        int type = getNameType(label);
+        int otherType = getNameType(otherLabel);
         if (type != otherType) {
             return Integer.compare(type, otherType);
         }
@@ -378,13 +669,17 @@
         return collator.compare(name == null ? "" : name, otherName == null ? "" : otherName);
     }
 
-    private static int getNameType(String displayName) {
+    /**
+     * Returns the type of the name string. Types can be {@link #TYPE_LETTER}, {@link #TYPE_DIGIT}
+     * and {@link #TYPE_OTHER}.
+     */
+    private static int getNameType(String label) {
         // A helper function to classify Contacts
-        if (!TextUtils.isEmpty(displayName)) {
-            if (Character.isLetter(displayName.charAt(0))) {
+        if (!TextUtils.isEmpty(label)) {
+            if (Character.isLetter(label.charAt(0))) {
                 return TYPE_LETTER;
             }
-            if (Character.isDigit(displayName.charAt(0))) {
+            if (label.contains("#")) {
                 return TYPE_DIGIT;
             }
         }
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 c137c5f..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
@@ -22,19 +22,22 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.Observer;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executors;
 
 /**
- * A singleton statically accessible helper class which pre-loads contacts list into memory so
- * that they can be accessed more easily and quickly.
+ * A singleton statically accessible helper class which pre-loads contacts list into memory so that
+ * they can be accessed more easily and quickly.
  */
 public class InMemoryPhoneBook implements Observer<List<Contact>> {
     private static final String TAG = "CD.InMemoryPhoneBook";
@@ -42,14 +45,21 @@
 
     private final Context mContext;
     private final AsyncQueryLiveData<List<Contact>> mContactListAsyncQueryLiveData;
-    /** A map to speed up phone number searching. */
+    /**
+     * A map to speed up phone number searching.
+     */
     private final Map<I18nPhoneNumberWrapper, Contact> mPhoneNumberContactMap = new HashMap<>();
+    /**
+     * A map to look up contact by account name and lookup key. Each entry presents a map of lookup
+     * key to contacts for one account.
+     */
+    private final Map<String, Map<String, Contact>> mLookupKeyContactMap = new HashMap<>();
     private boolean mIsLoaded = false;
 
     /**
-     * Initialize the globally accessible {@link InMemoryPhoneBook}.
-     * Returns the existing {@link InMemoryPhoneBook} if already initialized.
-     * {@link #tearDown()} must be called before init to reinitialize.
+     * Initialize the globally accessible {@link InMemoryPhoneBook}. Returns the existing {@link
+     * InMemoryPhoneBook} if already initialized. {@link #tearDown()} must be called before init to
+     * reinitialize.
      */
     public static InMemoryPhoneBook init(Context context) {
         if (sInMemoryPhoneBook == null) {
@@ -59,7 +69,19 @@
         return get();
     }
 
-    /** Get the global {@link InMemoryPhoneBook} instance. */
+    /**
+     * Returns if the InMemoryPhoneBook is initialized. get() won't return null or throw if this is
+     * true, but it doesn't indicate whether or not contacts are loaded yet.
+     * <p>
+     * See also: {@link #isLoaded()}
+     */
+    public static boolean isInitialized() {
+        return sInMemoryPhoneBook != null;
+    }
+
+    /**
+     * Get the global {@link InMemoryPhoneBook} instance.
+     */
     public static InMemoryPhoneBook get() {
         if (sInMemoryPhoneBook != null) {
             return sInMemoryPhoneBook;
@@ -68,7 +90,9 @@
         }
     }
 
-    /** Tears down the globally accessible {@link InMemoryPhoneBook}. */
+    /**
+     * Tears down the globally accessible {@link InMemoryPhoneBook}.
+     */
     public static void tearDown() {
         sInMemoryPhoneBook.onTearDown();
         sInMemoryPhoneBook = null;
@@ -80,11 +104,16 @@
         QueryParam contactListQueryParam = new QueryParam(
                 ContactsContract.Data.CONTENT_URI,
                 null,
-                ContactsContract.Data.MIMETYPE + " = ?",
-                new String[]{ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE},
+                ContactsContract.Data.MIMETYPE + " = ? OR "
+                        + ContactsContract.Data.MIMETYPE + " = ? OR "
+                        + ContactsContract.Data.MIMETYPE + " = ?",
+                new String[]{
+                        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
+                        ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
+                        ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE},
                 ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
         mContactListAsyncQueryLiveData = new AsyncQueryLiveData<List<Contact>>(mContext,
-                QueryParam.of(contactListQueryParam)) {
+                QueryParam.of(contactListQueryParam), Executors.newSingleThreadExecutor()) {
             @Override
             protected List<Contact> convertToEntity(Cursor cursor) {
                 return onCursorLoaded(cursor);
@@ -132,30 +161,85 @@
         return mPhoneNumberContactMap.get(i18nPhoneNumber);
     }
 
-    private List<Contact> onCursorLoaded(Cursor cursor) {
-        Map<String, Contact> result = new LinkedHashMap<>();
-        List<Contact> contacts = new ArrayList<>();
+    /**
+     * Looks up a {@link Contact} by the given lookup key and account name. Account name could be
+     * null for locally added contacts. Returns null if can't find the contact entry.
+     */
+    @Nullable
+    public Contact lookupContactByKey(String lookupKey, @Nullable String accountName) {
+        if (!isLoaded()) {
+            Log.w(TAG, "looking up a contact while loading.");
+        }
+        if (TextUtils.isEmpty(lookupKey)) {
+            Log.w(TAG, "looking up an empty lookup key.");
+            return null;
+        }
+        if (mLookupKeyContactMap.containsKey(accountName)) {
+            return mLookupKeyContactMap.get(accountName).get(lookupKey);
+        }
 
-        while (cursor.moveToNext()) {
-            Contact contact = Contact.fromCursor(mContext, cursor);
-            String lookupKey = contact.getLookupKey();
-            if (result.containsKey(lookupKey)) {
-                Contact existingContact = result.get(lookupKey);
-                existingContact.merge(contact);
-            } else {
-                result.put(lookupKey, contact);
+        return null;
+    }
+
+    /**
+     * Iterates all the accounts and returns a list of contacts that match the lookup key. This API
+     * is discouraged to use whenever the account name is available where {@link
+     * #lookupContactByKey(String, String)} should be used instead.
+     */
+    @NonNull
+    public List<Contact> lookupContactByKey(String lookupKey) {
+        if (!isLoaded()) {
+            Log.w(TAG, "looking up a contact while loading.");
+        }
+
+        if (TextUtils.isEmpty(lookupKey)) {
+            Log.w(TAG, "looking up an empty lookup key.");
+            return Collections.emptyList();
+        }
+        List<Contact> results = new ArrayList<>();
+        // Iterate all the accounts to get all the match contacts with given lookup key.
+        for (Map<String, Contact> subMap : mLookupKeyContactMap.values()) {
+            if (subMap.containsKey(lookupKey)) {
+                results.add(subMap.get(lookupKey));
             }
         }
 
-        contacts.addAll(result.values());
+        return results;
+    }
+
+    private List<Contact> onCursorLoaded(Cursor cursor) {
+        Map<String, Map<String, Contact>> contactMap = new LinkedHashMap<>();
+        List<Contact> contactList = new ArrayList<>();
+
+        while (cursor.moveToNext()) {
+            int accountNameColumn = cursor.getColumnIndex(
+                    ContactsContract.RawContacts.ACCOUNT_NAME);
+            int lookupKeyColumn = cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY);
+            String accountName = cursor.getString(accountNameColumn);
+            String lookupKey = cursor.getString(lookupKeyColumn);
+
+            if (!contactMap.containsKey(accountName)) {
+                contactMap.put(accountName, new HashMap<>());
+            }
+
+            Map<String, Contact> subMap = contactMap.get(accountName);
+            subMap.put(lookupKey, Contact.fromCursor(mContext, cursor, subMap.get(lookupKey)));
+        }
+
+        for (Map<String, Contact> subMap : contactMap.values()) {
+            contactList.addAll(subMap.values());
+        }
+
+        mLookupKeyContactMap.clear();
+        mLookupKeyContactMap.putAll(contactMap);
 
         mPhoneNumberContactMap.clear();
-        for (Contact contact : contacts) {
+        for (Contact contact : contactList) {
             for (PhoneNumber phoneNumber : contact.getNumbers()) {
                 mPhoneNumberContactMap.put(phoneNumber.getI18nPhoneNumberWrapper(), contact);
             }
         }
-        return contacts;
+        return contactList;
     }
 
     @Override
diff --git a/car-telephony-common/src/com/android/car/telephony/common/PhoneNumber.java b/car-telephony-common/src/com/android/car/telephony/common/PhoneNumber.java
index 85893e0..4e3eff6 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/PhoneNumber.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/PhoneNumber.java
@@ -23,7 +23,9 @@
 import android.os.Parcelable;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.text.TextUtils;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import java.util.Objects;
@@ -34,15 +36,21 @@
 public class PhoneNumber implements Parcelable {
 
     private final I18nPhoneNumberWrapper mI18nPhoneNumber;
-    private final int mType;
-    @Nullable
-    private final String mLabel;
+    @NonNull
+    private final String mAccountName;
+    @NonNull
+    private final String mAccountType;
 
+    private int mType;
+    @Nullable
+    private String mLabel;
     private boolean mIsPrimary;
     private long mId;
     private int mDataVersion;
-    private String mAccountName;
-    private String mAccountType;
+
+    /** The favorite bit is from local database, presenting a
+     *  {@link com.android.car.dialer.storage.FavoriteNumberEntity}. */
+    private boolean mIsFavorite;
 
     static PhoneNumber fromCursor(Context context, Cursor cursor) {
         int typeColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE);
@@ -99,22 +107,22 @@
         mLabel = label;
         mIsPrimary = isPrimary;
         mId = id;
-        mAccountName = accountName;
-        mAccountType = accountType;
+        mAccountName = TextUtils.emptyIfNull(accountName);
+        mAccountType = TextUtils.emptyIfNull(accountType);
         mDataVersion = dataVersion;
     }
 
     @Override
     public boolean equals(Object obj) {
         return obj instanceof PhoneNumber
-                && ((PhoneNumber) obj).mType == mType
-                && Objects.equals(((PhoneNumber) obj).mLabel, mLabel)
-                && mI18nPhoneNumber.equals(((PhoneNumber) obj).mI18nPhoneNumber);
+                && mI18nPhoneNumber.equals(((PhoneNumber) obj).mI18nPhoneNumber)
+                && mAccountName.equals(((PhoneNumber) obj).mAccountName)
+                && mAccountType.equals(((PhoneNumber) obj).mAccountType);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mI18nPhoneNumber, mType, mLabel);
+        return Objects.hash(mI18nPhoneNumber, mAccountName, mAccountType);
     }
 
     /**
@@ -176,6 +184,19 @@
     }
 
     /**
+     * Updates the favorite bit, which is local database. See
+     * {@link com.android.car.dialer.storage.FavoriteNumberDatabase}.
+     */
+    public void setIsFavorite(boolean isFavorite) {
+        mIsFavorite = isFavorite;
+    }
+
+    /** Returns if the phone number is favorite entry. */
+    public boolean isFavorite() {
+        return mIsFavorite;
+    }
+
+    /**
      * Each contact may have a few sources with the same phone number. Merge same phone numbers as
      * one.
      *
@@ -188,8 +209,8 @@
                 mDataVersion = phoneNumber.mDataVersion;
                 mId = phoneNumber.mId;
                 mIsPrimary |= phoneNumber.mIsPrimary;
-                mAccountName = phoneNumber.mAccountName;
-                mAccountType = phoneNumber.mAccountType;
+                mType = phoneNumber.mType;
+                mLabel = phoneNumber.mLabel;
             }
         }
         return this;
@@ -205,7 +226,7 @@
 
     @Override
     public String toString() {
-        return getNumber() + " " + String.valueOf(mLabel);
+        return getNumber() + " " + mAccountName + " " + mAccountType;
     }
 
     @Override
@@ -223,6 +244,7 @@
         dest.writeString(mAccountName);
         dest.writeString(mAccountType);
         dest.writeInt(mDataVersion);
+        dest.writeBoolean(mIsFavorite);
     }
 
     public static Creator<PhoneNumber> CREATOR = new Creator<PhoneNumber>() {
@@ -239,6 +261,7 @@
             int dataVersion = source.readInt();
             PhoneNumber phoneNumber = new PhoneNumber(i18nPhoneNumberWrapper, type, label,
                     isPrimary, id, accountName, accountType, dataVersion);
+            phoneNumber.setIsFavorite(source.readBoolean());
             return phoneNumber;
         }
 
diff --git a/car-telephony-common/src/com/android/car/telephony/common/PostalAddress.java b/car-telephony-common/src/com/android/car/telephony/common/PostalAddress.java
new file mode 100644
index 0000000..cb7c774
--- /dev/null
+++ b/car-telephony-common/src/com/android/car/telephony/common/PostalAddress.java
@@ -0,0 +1,156 @@
+/*
+ * 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.telephony.common;
+
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Encapsulates data about an address entry. Typically loaded from the local Address store.
+ */
+public class PostalAddress implements Parcelable {
+    private static final String TAG = "CD.PostalAddress";
+
+    /**
+     * The formatted address.
+     */
+    private String mFormattedAddress;
+
+    /**
+     * The address type. See more at {@link ContactsContract.CommonDataKinds.StructuredPostal#TYPE}
+     */
+    private int mType;
+
+    /**
+     * The user defined label. See more at
+     * {@link ContactsContract.CommonDataKinds.StructuredPostal#LABEL}
+     */
+    @Nullable
+    private String mLabel;
+
+    /**
+     * Parses a PostalAddress entry for a Cursor loaded from the Address Database.
+     */
+    public static PostalAddress fromCursor(Cursor cursor) {
+        int formattedAddressColumn = cursor.getColumnIndex(
+                ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS);
+        int addressTypeColumn = cursor.getColumnIndex(
+                ContactsContract.CommonDataKinds.StructuredPostal.TYPE);
+        int labelColumn = cursor.getColumnIndex(
+                ContactsContract.CommonDataKinds.StructuredPostal.LABEL);
+
+        PostalAddress postalAddress = new PostalAddress();
+        postalAddress.mFormattedAddress = cursor.getString(formattedAddressColumn);
+        postalAddress.mType = cursor.getInt(addressTypeColumn);
+        postalAddress.mLabel = cursor.getString(labelColumn);
+
+        return postalAddress;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof PostalAddress
+                && mFormattedAddress.equals(((PostalAddress) obj).mFormattedAddress);
+    }
+
+    /**
+     * Returns {@link #mFormattedAddress}
+     */
+    public String getFormattedAddress() {
+        return mFormattedAddress;
+    }
+
+    /**
+     * Returns {@link #mType}
+     */
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Returns {@link #mLabel}
+     */
+    @Nullable
+    public String getLabel() {
+        return mLabel;
+    }
+
+    /**
+     * Returns a human readable string label. For example, Home, Work, etc.
+     */
+    public CharSequence getReadableLabel(Resources res) {
+        return ContactsContract.CommonDataKinds.StructuredPostal.getTypeLabel(res, mType, mLabel);
+    }
+
+    /**
+     * Returns the address Uri for {@link #mFormattedAddress}.
+     */
+    public Uri getAddressUri(Resources res) {
+        String address = String.format(res.getString(R.string.address_uri_format),
+                Uri.encode(mFormattedAddress));
+        Log.d(TAG, "The address is: " + address);
+        return Uri.parse(address);
+    }
+
+    /**
+     * Returns the navigation Uri for {@link #mFormattedAddress}.
+     */
+    public Uri getNavigationUri(Resources res) {
+        String address = String.format(res.getString(R.string.navigation_uri_format),
+                Uri.encode(mFormattedAddress));
+        Log.d(TAG, "The address is: " + address);
+        return Uri.parse(address);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mType);
+        dest.writeString(mLabel);
+        dest.writeString(mFormattedAddress);
+    }
+
+    /**
+     * Create {@link PostalAddress} object from saved parcelable.
+     */
+    public static Creator<PostalAddress> CREATOR = new Creator<PostalAddress>() {
+        @Override
+        public PostalAddress createFromParcel(Parcel source) {
+            PostalAddress postalAddress = new PostalAddress();
+            postalAddress.mType = source.readInt();
+            postalAddress.mLabel = source.readString();
+            postalAddress.mFormattedAddress = source.readString();
+            return postalAddress;
+        }
+
+        @Override
+        public PostalAddress[] newArray(int size) {
+            return new PostalAddress[size];
+        }
+    };
+}
diff --git a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
index d832a42..090f223 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/TelecomUtils.java
@@ -24,6 +24,11 @@
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Icon;
+import android.location.Country;
+import android.location.CountryDetector;
 import android.net.Uri;
 import android.provider.CallLog;
 import android.provider.ContactsContract;
@@ -38,7 +43,9 @@
 import android.widget.ImageView;
 
 import androidx.annotation.Nullable;
-import androidx.core.util.Pair;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
 
 import com.android.car.apps.common.LetterTileDrawable;
 
@@ -51,59 +58,18 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
 
-/** Helper methods. */
+/**
+ * Helper methods.
+ */
 public class TelecomUtils {
     private static final String TAG = "CD.TelecomUtils";
 
-    private static final String[] CONTACT_ID_PROJECTION = new String[]{
-            PhoneLookup.DISPLAY_NAME,
-            PhoneLookup.TYPE,
-            PhoneLookup.LABEL,
-            PhoneLookup._ID
-    };
-
     private static String sVoicemailNumber;
     private static TelephonyManager sTelephonyManager;
 
     /**
-     * Return the label for the given phone number.
-     *
-     * @param number Caller phone number
-     * @return the label if it is found, empty string otherwise.
-     */
-    public static CharSequence getTypeFromNumber(Context context, String number) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "getTypeFromNumber, number: " + number);
-        }
-
-        String defaultLabel = "";
-        if (TextUtils.isEmpty(number)) {
-            return defaultLabel;
-        }
-
-        ContentResolver cr = context.getContentResolver();
-        Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
-        Cursor cursor = cr.query(uri, CONTACT_ID_PROJECTION, null, null, null);
-
-        try {
-            if (cursor != null && cursor.moveToFirst()) {
-                int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
-                int type = cursor.getInt(typeColumn);
-                int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
-                String label = cursor.getString(labelColumn);
-                CharSequence typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
-                return typeLabel;
-            }
-        } finally {
-            if (cursor != null) {
-                cursor.close();
-            }
-        }
-        return defaultLabel;
-    }
-
-    /**
      * Get the voicemail number.
      */
     public static String getVoicemailNumber(Context context) {
@@ -119,7 +85,16 @@
      * @see TelephonyManager#getVoiceMailNumber()
      */
     public static boolean isVoicemailNumber(Context context, String number) {
-        return !TextUtils.isEmpty(number) && number.equals(getVoicemailNumber(context));
+        if (TextUtils.isEmpty(number)) {
+            return false;
+        }
+
+        if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE)
+                != PackageManager.PERMISSION_GRANTED) {
+            return false;
+        }
+
+        return number.equals(getVoicemailNumber(context));
     }
 
     /**
@@ -145,7 +120,7 @@
             return "";
         }
 
-        String countryIso = getIsoDefaultCountryNumber(context);
+        String countryIso = getCurrentCountryIso(context);
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "PhoneNumberUtils.formatNumberToE16, number: "
                     + number + ", country: " + countryIso);
@@ -159,85 +134,201 @@
         return formattedNumber;
     }
 
-    private static String getIsoDefaultCountryNumber(Context context) {
-        String countryIso = getTelephonyManager(context).getSimCountryIso().toUpperCase(Locale.US);
-        if (countryIso.length() != 2) {
-            countryIso = Locale.getDefault().getCountry();
-            if (countryIso == null || countryIso.length() != 2) {
-                countryIso = "US";
+    /**
+     * @return The ISO 3166-1 two letters country code of the country the user is in.
+     */
+    private static String getCurrentCountryIso(Context context, Locale locale) {
+        String countryIso = null;
+        CountryDetector detector = (CountryDetector) context.getSystemService(
+                Context.COUNTRY_DETECTOR);
+        if (detector != null) {
+            Country country = detector.detectCountry();
+            if (country != null) {
+                countryIso = country.getCountryIso();
+            } else {
+                Log.e(TAG, "CountryDetector.detectCountry() returned null.");
             }
         }
-
+        if (countryIso == null) {
+            countryIso = locale.getCountry();
+            Log.w(TAG, "No CountryDetector; falling back to countryIso based on locale: "
+                    + countryIso);
+        }
+        if (countryIso == null || countryIso.length() != 2) {
+            Log.w(TAG, "Invalid locale, falling back to US");
+            countryIso = "US";
+        }
         return countryIso;
     }
 
+    private static String getCurrentCountryIso(Context context) {
+        return getCurrentCountryIso(context, Locale.getDefault());
+    }
+
     /**
-     * Creates a new instance of {@link Phonenumber#Phonenumber} base on the given number and sim
+     * Creates a new instance of {@link Phonenumber.PhoneNumber} base on the given number and sim
      * card country code. Returns {@code null} if the number in an invalid number.
      */
     @Nullable
     public static Phonenumber.PhoneNumber createI18nPhoneNumber(Context context, String number) {
         try {
-            return PhoneNumberUtil.getInstance().parse(number, getIsoDefaultCountryNumber(context));
+            return PhoneNumberUtil.getInstance().parse(number, getCurrentCountryIso(context));
         } catch (NumberParseException e) {
             return null;
         }
     }
 
     /**
-     * Get the display name and photo uri of the given number (e.g. if it's the voicemail number,
-     * return a string and a uri that represents voicemail, if it's a contact, get the contact's
-     * name and its avatar uri, etc).
-     *
-     * @return Pair of display name and contact's photo uri if found. Voicemail number uses drawable
-     * resource uri and null uri for other cases.
+     * Contains all the info used to display a phone number on the screen. Returned by {@link
+     * #getPhoneNumberInfo(Context, String)}
      */
-    public static Pair<String, Uri> getDisplayNameAndAvatarUri(Context context, String number) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "getDisplayNameAndAvatarUri: " + number);
+    public static final class PhoneNumberInfo {
+        private final String mPhoneNumber;
+        private final String mDisplayName;
+        private final String mInitials;
+        private final Uri mAvatarUri;
+        private final String mTypeLabel;
+
+        public PhoneNumberInfo(String phoneNumber, String displayName,
+                String initials, Uri avatarUri, String typeLabel) {
+            mPhoneNumber = phoneNumber;
+            mDisplayName = displayName;
+            mInitials = initials;
+            mAvatarUri = avatarUri;
+            mTypeLabel = typeLabel;
         }
 
+        public String getPhoneNumber() {
+            return mPhoneNumber;
+        }
+
+        public String getDisplayName() {
+            return mDisplayName;
+        }
+
+        /**
+         * Returns the initials of the contact related to the phone number. Returns null if there is
+         * no related contact.
+         */
+        @Nullable
+        public String getInitials() {
+            return mInitials;
+        }
+
+        @Nullable
+        public Uri getAvatarUri() {
+            return mAvatarUri;
+        }
+
+        public String getTypeLabel() {
+            return mTypeLabel;
+        }
+
+    }
+
+    /**
+     * Gets all the info needed to properly display a phone number to the UI. (e.g. if it's the
+     * voicemail number, return a string and a uri that represents voicemail, if it's a contact, get
+     * the contact's name, its avatar uri, the phone number's label, etc).
+     */
+    public static CompletableFuture<PhoneNumberInfo> getPhoneNumberInfo(
+            Context context, String number) {
+
         if (TextUtils.isEmpty(number)) {
-            return new Pair<>(context.getString(R.string.unknown), null);
+            return CompletableFuture.completedFuture(new PhoneNumberInfo(
+                    number,
+                    context.getString(R.string.unknown),
+                    null,
+                    null,
+                    ""));
         }
 
         if (isVoicemailNumber(context, number)) {
-            return new Pair<>(
+            return CompletableFuture.completedFuture(new PhoneNumberInfo(
+                    number,
                     context.getString(R.string.voicemail),
-                    makeResourceUri(context, R.drawable.ic_voicemail));
+                    null,
+                    makeResourceUri(context, R.drawable.ic_voicemail),
+                    ""));
         }
 
-        ContentResolver cr = context.getContentResolver();
-        Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
+        if (InMemoryPhoneBook.isInitialized()) {
+            Contact contact = InMemoryPhoneBook.get().lookupContactEntry(number);
+            if (contact != null) {
+                String name = contact.getDisplayName();
+                if (name == null) {
+                    name = getFormattedNumber(context, number);
+                }
 
-        Cursor cursor = null;
-        String name = null;
-        String photoUriString = null;
-        try {
-            cursor = cr.query(uri, new String[]{PhoneLookup.DISPLAY_NAME, PhoneLookup.PHOTO_URI},
-                    null, null, null);
-            if (cursor != null && cursor.moveToFirst()) {
-                name = cursor.getString(0);
-                photoUriString = cursor.getString(1);
-            }
-        } finally {
-            if (cursor != null) {
-                cursor.close();
+                if (name == null) {
+                    name = context.getString(R.string.unknown);
+                }
+
+                PhoneNumber phoneNumber = contact.getPhoneNumber(context, number);
+                CharSequence typeLabel = "";
+                if (phoneNumber != null) {
+                    typeLabel = Phone.getTypeLabel(context.getResources(),
+                            phoneNumber.getType(),
+                            phoneNumber.getLabel());
+                }
+
+                return CompletableFuture.completedFuture(new PhoneNumberInfo(
+                        number,
+                        name,
+                        contact.getInitials(),
+                        contact.getAvatarUri(),
+                        typeLabel.toString()));
             }
         }
 
-        if (name == null) {
-            name = getFormattedNumber(context, number);
-        }
+        return CompletableFuture.supplyAsync(() -> {
+            String name = null;
+            String nameAlt = null;
+            String photoUriString = null;
+            CharSequence typeLabel = "";
+            ContentResolver cr = context.getContentResolver();
+            String initials;
+            try (Cursor cursor = cr.query(
+                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+                    new String[]{
+                            PhoneLookup.DISPLAY_NAME,
+                            PhoneLookup.DISPLAY_NAME_ALTERNATIVE,
+                            PhoneLookup.PHOTO_URI,
+                            PhoneLookup.TYPE,
+                            PhoneLookup.LABEL,
+                    },
+                    null, null, null)) {
 
-        if (name == null) {
-            name = context.getString(R.string.unknown);
-        }
+                if (cursor != null && cursor.moveToFirst()) {
+                    int nameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
+                    int altNameColumn = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME_ALTERNATIVE);
+                    int photoUriColumn = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
+                    int typeColumn = cursor.getColumnIndex(PhoneLookup.TYPE);
+                    int labelColumn = cursor.getColumnIndex(PhoneLookup.LABEL);
 
-        if (TextUtils.isEmpty(photoUriString)) {
-            return new Pair<>(name, null);
-        }
-        return new Pair<>(name, Uri.parse(photoUriString));
+                    name = cursor.getString(nameColumn);
+                    nameAlt = cursor.getString(altNameColumn);
+                    photoUriString = cursor.getString(photoUriColumn);
+                    int type = cursor.getInt(typeColumn);
+                    String label = cursor.getString(labelColumn);
+                    typeLabel = Phone.getTypeLabel(context.getResources(), type, label);
+                }
+            }
+
+            initials = getInitials(name, nameAlt);
+
+            if (name == null) {
+                name = getFormattedNumber(context, number);
+            }
+
+            if (name == null) {
+                name = context.getString(R.string.unknown);
+            }
+
+            return new PhoneNumberInfo(number, name, initials,
+                    TextUtils.isEmpty(photoUriString) ? null : Uri.parse(photoUriString),
+                    typeLabel.toString());
+        });
     }
 
     /**
@@ -286,53 +377,80 @@
     }
 
     /**
-     * Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's
-     * display name or {@code fallbackDisplayName} will be used as a fallback resource if avatar
-     * loading fails.
+     * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the
+     * contact's initials.
      */
     public static void setContactBitmapAsync(
             Context context,
-            final ImageView icon,
-            @Nullable final Contact contact,
-            @Nullable final String fallbackDisplayName) {
-        Uri avatarUri = contact != null ? contact.getAvatarUri() : null;
-        String displayName = contact != null ? contact.getDisplayName() : fallbackDisplayName;
-
-        setContactBitmapAsync(context, icon, avatarUri, displayName);
+            @Nullable final ImageView icon,
+            @Nullable final Contact contact) {
+        setContactBitmapAsync(context, icon, contact, null);
     }
 
     /**
-     * Sets a Contact avatar onto the provided {@code icon}. The first letter of the contact's
-     * display name will be used as a fallback resource if avatar loading fails.
+     * Sets a Contact avatar onto the provided {@code icon}. The first letter or both letters of the
+     * contact's initials or {@code fallbackDisplayName} will be used as a fallback resource if
+     * avatar loading fails.
      */
     public static void setContactBitmapAsync(
             Context context,
-            final ImageView icon,
-            final Uri avatarUri,
-            final String displayName) {
-        LetterTileDrawable letterTileDrawable = createLetterTile(context, displayName);
+            @Nullable final ImageView icon,
+            @Nullable final Contact contact,
+            @Nullable final String fallbackDisplayName) {
+        Uri avatarUri = contact != null ? contact.getAvatarUri() : null;
+        String initials = contact != null ? contact.getInitials()
+                : (fallbackDisplayName == null ? null : getInitials(fallbackDisplayName, null));
+        String identifier = contact == null ? fallbackDisplayName : contact.getDisplayName();
 
-        if (avatarUri != null) {
-            Glide.with(context)
-                    .load(avatarUri)
-                    .apply(new RequestOptions().centerCrop().error(letterTileDrawable))
-                    .into(icon);
+        setContactBitmapAsync(context, icon, avatarUri, initials, identifier);
+    }
+
+    /**
+     * Sets a Contact avatar onto the provided {@code icon}. A letter tile base on the contact's
+     * initials and identifier will be used as a fallback resource if avatar loading fails.
+     */
+    public static void setContactBitmapAsync(
+            Context context,
+            @Nullable final ImageView icon,
+            @Nullable final Uri avatarUri,
+            @Nullable final String initials,
+            @Nullable final String identifier) {
+        if (icon == null) {
             return;
         }
 
-        // Use the letter tile as avatar if there is no avatar available from content provider.
-        icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
-        icon.setImageDrawable(letterTileDrawable);
+        LetterTileDrawable letterTileDrawable = createLetterTile(context, initials, identifier);
+
+        Glide.with(context)
+                .load(avatarUri)
+                .apply(new RequestOptions().centerCrop().error(letterTileDrawable))
+                .into(icon);
     }
 
-    /** Create a {@link LetterTileDrawable} for the given display name. */
-    public static LetterTileDrawable createLetterTile(Context context, String displayName) {
-        LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources());
-        letterTileDrawable.setContactDetails(displayName, displayName);
-        return  letterTileDrawable;
+    /**
+     * 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.
+     */
+    public static LetterTileDrawable createLetterTile(
+            Context context,
+            @Nullable String initials,
+            @Nullable String identifier) {
+        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;
     }
 
-    /** Set the given phone number as the primary phone number for its associated contact. */
+    /**
+     * Set the given phone number as the primary phone number for its associated contact.
+     */
     public static void setAsPrimaryPhoneNumber(Context context, PhoneNumber phoneNumber) {
         // Update the primary values in the data record.
         ContentValues values = new ContentValues(1);
@@ -344,7 +462,9 @@
                 values, null, null);
     }
 
-    /** Add a contact to favorite or remove it from favorite. */
+    /**
+     * Add a contact to favorite or remove it from favorite.
+     */
     public static int setAsFavoriteContact(Context context, Contact contact, boolean isFavorite) {
         if (contact.isStarred() == isFavorite) {
             return 0;
@@ -400,6 +520,53 @@
         }
     }
 
+    /**
+     * 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) {
+        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();
+    }
+
+    /**
+     * 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 Icon createLetterTile(Context context, @Nullable String initials,
+            String identifier, int avatarSize, float cornerRadiusPercent) {
+        LetterTileDrawable letterTileDrawable = TelecomUtils.createLetterTile(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. **/
+    public static Icon createFromRoundedBitmapDrawable(RoundedBitmapDrawable roundedBitmapDrawable,
+            int avatarSize, float cornerRadiusPercent) {
+        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 Icon.createWithBitmap(result);
+    }
+
     private static Uri makeResourceUri(Context context, int resourceId) {
         return new Uri.Builder()
                 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
diff --git a/car-telephony-common/src/com/android/car/telephony/common/WorkerExecutor.java b/car-telephony-common/src/com/android/car/telephony/common/WorkerExecutor.java
new file mode 100644
index 0000000..7bb91c7
--- /dev/null
+++ b/car-telephony-common/src/com/android/car/telephony/common/WorkerExecutor.java
@@ -0,0 +1,53 @@
+/*
+ * 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.telephony.common;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * WorkerExecutor is a singleton tied to the application to provide {@link ExecutorService} for
+ * Dialer to run tasks in background.
+ */
+public class WorkerExecutor {
+    private static WorkerExecutor sWorkerExecutor;
+
+    private ExecutorService mSingleThreadExecutor;
+
+    /** Returns the singleton WorkerExecutor for the application. */
+    public static WorkerExecutor getInstance() {
+        if (sWorkerExecutor == null) {
+            sWorkerExecutor = new WorkerExecutor();
+        }
+        return sWorkerExecutor;
+    }
+
+    private WorkerExecutor() {
+        mSingleThreadExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    /** Returns the single thread executor. */
+    public ExecutorService getSingleThreadExecutor() {
+        return mSingleThreadExecutor;
+    }
+
+    /** Tears down the singleton WorkerExecutor for the application */
+    public void tearDown() {
+        mSingleThreadExecutor.shutdown();
+        sWorkerExecutor = null;
+    }
+}
diff --git a/car-ui-lib/.gitignore b/car-ui-lib/.gitignore
new file mode 100644
index 0000000..cedb234
--- /dev/null
+++ b/car-ui-lib/.gitignore
@@ -0,0 +1,15 @@
+# Local configuration
+local.properties
+gradle-wrapper.properties
+
+# Gradle
+gradle/
+.gradle/
+build/
+
+# IntelliJ
+.idea/
+*.iml
+
+# Python
+*.pyc
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/Android.mk b/car-ui-lib/Android.mk
new file mode 100644
index 0000000..c67ea5c
--- /dev/null
+++ b/car-ui-lib/Android.mk
@@ -0,0 +1,75 @@
+#
+# 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.
+#
+
+# Including generate_rros.mk utility.
+# Usage:
+#
+# LOCAL_PATH := $(call my-dir)
+#
+# CAR_UI_RRO_SET_NAME := sample
+# CAR_UI_RESOURCE_DIR := $(LOCAL_PATH)/res
+# CAR_UI_RRO_TARGETS := \
+#   com.your.package.name.1 \
+#   com.your.package.name.2 \
+#   com.your.package.name.3
+#
+# include $(CAR_UI_GENERATE_RRO_SET)
+#
+# Your AndroidManifest must use {{TARGET_PACKAGE_NAME}} and {{RRO_PACKAGE_NAME}}
+# tags, which will be replaced accordingly during build.
+
+CAR_UI_GENERATE_RRO_SET := $(call my-dir)/generate_rros.mk
+
+# Build car-ui library
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_MODULE := car-ui-lib
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_JAVA_LIBRARIES += android.car
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_STATIC_ANDROID_LIBRARIES += \
+    androidx.annotation_annotation \
+    androidx.appcompat_appcompat \
+    androidx-constraintlayout_constraintlayout \
+    androidx.preference_preference \
+    androidx.recyclerview_recyclerview \
+    androidx.asynclayoutinflater_asynclayoutinflater \
+
+LOCAL_STATIC_JAVA_LIBRARIES += \
+    androidx-constraintlayout_constraintlayout-solver \
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+ifeq (,$(ONE_SHOT_MAKEFILE))
+    include $(call all-makefiles-under,$(LOCAL_PATH))
+endif
diff --git a/car-media-common/res/values-h668dp/dimens.xml b/car-ui-lib/AndroidManifest-gradle.xml
similarity index 63%
copy from car-media-common/res/values-h668dp/dimens.xml
copy to car-ui-lib/AndroidManifest-gradle.xml
index 3ca1445..b6e8ee6 100644
--- a/car-media-common/res/values-h668dp/dimens.xml
+++ b/car-ui-lib/AndroidManifest-gradle.xml
@@ -1,21 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright 2019, The Android Open Source Project
+  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
+    http://www.apache.org/licenses/LICENSE-2.0
 
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT 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>
-    <!-- App bar -->
-    <!-- The height of app bar when it expends to 2 rows. Equals 2 * @dimen/appbar_first_row_height. -->
-    <dimen name="appbar_2_rows_height">192dp</dimen>
-</resources>
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.ui">
+</manifest>
diff --git a/car-media-common/res/values-h668dp/dimens.xml b/car-ui-lib/AndroidManifest.xml
similarity index 62%
copy from car-media-common/res/values-h668dp/dimens.xml
copy to car-ui-lib/AndroidManifest.xml
index 3ca1445..038ee67 100644
--- a/car-media-common/res/values-h668dp/dimens.xml
+++ b/car-ui-lib/AndroidManifest.xml
@@ -1,21 +1,23 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright 2019, The Android Open Source Project
+  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
+    http://www.apache.org/licenses/LICENSE-2.0
 
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT 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>
-    <!-- App bar -->
-    <!-- The height of app bar when it expends to 2 rows. Equals 2 * @dimen/appbar_first_row_height. -->
-    <dimen name="appbar_2_rows_height">192dp</dimen>
-</resources>
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.ui">
+  <uses-sdk
+      android:minSdkVersion="14"
+      android:targetSdkVersion="28" />
+</manifest>
diff --git a/car-ui-lib/OWNERS b/car-ui-lib/OWNERS
new file mode 100644
index 0000000..aef5c65
--- /dev/null
+++ b/car-ui-lib/OWNERS
@@ -0,0 +1,9 @@
+# People who can approve changes for submission.
+
+# Engs
+colefaust@google.com
+priyanksingh@google.com
+rampara@google.com
+ckoessler@google.com
+
+
diff --git a/car-ui-lib/README.md b/car-ui-lib/README.md
new file mode 100644
index 0000000..282f8e1
--- /dev/null
+++ b/car-ui-lib/README.md
@@ -0,0 +1,28 @@
+# Android Automotive 'Chassis' library
+Components and resources designed to increase Automotive UI consistency between
+GAS (Google Automotive Services) apps, system-apps and OEM developed apps.
+
+See: go/aae-carui
+
+## Content
+
+Components and resources designed to be configured by means of RRO (Runtime
+Resource Overlays) by OEMs.
+
+## Updating
+
+This library is developed in Gerrit and copied as source to Google3 using
+Copybara (go/copybara).
+
+Source: /packages/apps/Car/libs/car-ui-lib
+Target: //google3/third_party/java/android_libs/android_car_chassis_lib
+
+Here is the process for updating this library:
+
+1. Develop, test and upload changes to Gerrit
+2. On Google3, run './update.sh review <cl>' (with <cl> being your Gerrit CL #) and test your changes
+3. Repeat #1 and #2 until your changes look okay on both places.
+4. Back on Gerrit, submit your CL.
+5. Back on Google3, run './update.sh manual' submit
+
+TODO: Automate this process using CaaS (in progress)
diff --git a/car-ui-lib/TEST_MAPPING b/car-ui-lib/TEST_MAPPING
new file mode 100644
index 0000000..dd3e2be
--- /dev/null
+++ b/car-ui-lib/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "generate-resources"
+    }
+  ]
+}
diff --git a/car-ui-lib/build.gradle b/car-ui-lib/build.gradle
new file mode 100644
index 0000000..7775382
--- /dev/null
+++ b/car-ui-lib/build.gradle
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.1'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+    buildDir = "/tmp/car-ui-build/${rootProject.name}/${project.name}"
+    tasks.withType(JavaCompile) {
+        options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+    }
+}
+
+// Library-level build file
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+
+    defaultConfig {
+        minSdkVersion 28
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    sourceSets {
+        main {
+            manifest.srcFile 'AndroidManifest-gradle.xml'
+            java.srcDirs = ['src']
+            res.srcDirs = ['res']
+        }
+    }
+}
+
+dependencies {
+    api 'androidx.annotation:annotation:1.1.0'
+    api 'androidx.appcompat:appcompat:1.1.0'
+    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/generate_rros.mk b/car-ui-lib/generate_rros.mk
new file mode 100644
index 0000000..4f67520
--- /dev/null
+++ b/car-ui-lib/generate_rros.mk
@@ -0,0 +1,54 @@
+#
+# 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.
+#
+
+# Generates one RRO for a given package.
+# $(1) target package name
+# $(2) name of the RRO set (e.g. "base")
+# $(3) resources folder
+define generate-rro
+  include $$(CLEAR_VARS)
+
+  rro_package_name := $(2)-$(subst .,-,$(1))
+  LOCAL_RESOURCE_DIR := $(3)
+  LOCAL_RRO_THEME := $$(rro_package_name)
+  LOCAL_PACKAGE_NAME := $$(rro_package_name)
+  LOCAL_CERTIFICATE := platform
+  LOCAL_SDK_VERSION := current
+  LOCAL_USE_AAPT2 := true
+
+  # Add --no-resource-deduping to prevent overlays to "values-port" with the same
+  # value as in "values" from being removed
+  LOCAL_AAPT_FLAGS := --no-resource-deduping
+
+  gen := $$(call intermediates-dir-for,ETC,$$(rro_package_name))/AndroidManifest.xml
+  $$(gen): $(LOCAL_PATH)/AndroidManifest.xml
+	@echo Generate $$@
+	$$(hide) mkdir -p $$(dir $$@)
+	$$(hide) sed -e "s/{{TARGET_PACKAGE_NAME}}/$(1)/" \
+	             -e "s/{{RRO_PACKAGE_NAME}}/$(1).$(2).rro/" $$< > $$@
+  LOCAL_FULL_MANIFEST_FILE := $$(gen)
+
+  include $$(BUILD_RRO_PACKAGE)
+endef
+
+$(foreach t,\
+  $(CAR_UI_RRO_TARGETS),\
+  $(eval $(call generate-rro,$(t),$(CAR_UI_RRO_SET_NAME),$(CAR_UI_RESOURCE_DIR))))
+
+# Clear variables
+CAR_UI_RRO_SET_NAME :=
+CAR_UI_RESOURCE_DIR :=
+CAR_UI_RRO_TARGETS :=
diff --git a/car-ui-lib/gradle.properties b/car-ui-lib/gradle.properties
new file mode 100644
index 0000000..9dad1c4
--- /dev/null
+++ b/car-ui-lib/gradle.properties
@@ -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.
+
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
diff --git a/car-ui-lib/gradlew b/car-ui-lib/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/car-ui-lib/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/car-ui-lib/gradlew.bat b/car-ui-lib/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/car-ui-lib/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windows variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/car-ui-lib/res/color/car_ui_color_accent.xml b/car-ui-lib/res/color/car_ui_color_accent.xml
new file mode 100644
index 0000000..9c227cf
--- /dev/null
+++ b/car-ui-lib/res/color/car_ui_color_accent.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.
+-->
+<!-- We need this to be a selector instead of a straight reference or else the
+     app will crash when using it as a TextView's text color -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?android:attr/colorAccent"/>
+</selector>
diff --git a/car-ui-lib/res/color/car_ui_list_item_divider.xml b/car-ui-lib/res/color/car_ui_list_item_divider.xml
new file mode 100644
index 0000000..7f0f443
--- /dev/null
+++ b/car-ui-lib/res/color/car_ui_list_item_divider.xml
@@ -0,0 +1,18 @@
+<?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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:alpha="0.22" android:color="@android:color/white" />
+</selector>
diff --git a/car-ui-lib/res/color/car_ui_text_color_hint.xml b/car-ui-lib/res/color/car_ui_text_color_hint.xml
new file mode 100644
index 0000000..5292699
--- /dev/null
+++ b/car-ui-lib/res/color/car_ui_text_color_hint.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.
+-->
+<!-- We need this to be a selector instead of a straight reference or else the
+     app will crash when using it as a TextView's text color -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?android:attr/textColorHint"/>
+</selector>
diff --git a/car-ui-lib/res/color/car_ui_text_color_primary.xml b/car-ui-lib/res/color/car_ui_text_color_primary.xml
new file mode 100644
index 0000000..d310838
--- /dev/null
+++ b/car-ui-lib/res/color/car_ui_text_color_primary.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+<!-- Copy of ?android:attr/textColorPrimary (frameworks/base/res/res/color/text_color_primary.xml)
+     but with a ux restricted state. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item android:state_enabled="false"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item android:color="?android:attr/colorForeground"/>
+</selector>
diff --git a/car-ui-lib/res/color/car_ui_text_color_secondary.xml b/car-ui-lib/res/color/car_ui_text_color_secondary.xml
new file mode 100644
index 0000000..4c9f267
--- /dev/null
+++ b/car-ui-lib/res/color/car_ui_text_color_secondary.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+<!-- Copy of ?android:attr/textColorSecondary (frameworks/base/res/res/color/text_color_secondary.xml)
+     but with a ux restricted state. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item android:state_enabled="false"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item android:alpha="?android:attr/secondaryContentAlpha"
+          android:color="?android:attr/colorForeground"/>
+</selector>
diff --git a/car-ui-lib/res/color/car_ui_toolbar_menu_item_icon_background_color.xml b/car-ui-lib/res/color/car_ui_toolbar_menu_item_icon_background_color.xml
new file mode 100644
index 0000000..5a8e81e
--- /dev/null
+++ b/car-ui-lib/res/color/car_ui_toolbar_menu_item_icon_background_color.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+<!-- The same as @color/car_ui_text_color_primary but with an activated state.
+     ColorStateLists don't support switching to complex colors, so we have to repeat
+     car_ui_text_color_primary here. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item android:state_activated="false"
+          android:color="@android:color/transparent"/>
+    <item android:state_enabled="false"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item android:color="?android:attr/colorForeground" />
+</selector>
diff --git a/car-ui-lib/res/color/car_ui_toolbar_menu_item_icon_color.xml b/car-ui-lib/res/color/car_ui_toolbar_menu_item_icon_color.xml
new file mode 100644
index 0000000..d6e5dea
--- /dev/null
+++ b/car-ui-lib/res/color/car_ui_toolbar_menu_item_icon_color.xml
@@ -0,0 +1,31 @@
+<?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.
+  -->
+<!-- The same as @color/car_ui_text_color_primary but with an activated state.
+     ColorStateLists don't support switching to complex colors, so we have to repeat
+     car_ui_text_color_primary here. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item android:state_activated="true"
+          android:color="?android:attr/colorBackground"/>
+    <item android:state_enabled="false"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item app:state_ux_restricted="true"
+          android:alpha="?android:attr/disabledAlpha"
+          android:color="?android:attr/colorForeground"/>
+    <item android:color="?android:attr/colorForeground" />
+</selector>
diff --git a/car-ui-lib/res/color/car_ui_toolbar_tab_item_selector.xml b/car-ui-lib/res/color/car_ui_toolbar_tab_item_selector.xml
new file mode 100644
index 0000000..47d1a8e
--- /dev/null
+++ b/car-ui-lib/res/color/car_ui_toolbar_tab_item_selector.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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/car_ui_toolbar_tab_selected_color" android:state_activated="true"/>
+    <item android:color="@color/car_ui_toolbar_tab_unselected_color"/>
+</selector>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_divider.xml
similarity index 65%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_divider.xml
index c5d298b..164b71a 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_divider.xml
@@ -14,12 +14,10 @@
   ~ 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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <size android:height="2dp"
+          android:width="2dp"/>
+    <solid android:color="@android:color/transparent" />
+</shape>
diff --git a/car-media-common/res/drawable/ic_media_select_arrow_drop_down.xml b/car-ui-lib/res/drawable/car_ui_icon_arrow_back.xml
similarity index 86%
rename from car-media-common/res/drawable/ic_media_select_arrow_drop_down.xml
rename to car-ui-lib/res/drawable/car_ui_icon_arrow_back.xml
index 2774d0f..4ad49b2 100644
--- a/car-media-common/res/drawable/ic_media_select_arrow_drop_down.xml
+++ b/car-ui-lib/res/drawable/car_ui_icon_arrow_back.xml
@@ -15,11 +15,12 @@
   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"
         android:viewportHeight="24.0">
     <path
         android:fillColor="#FF000000"
-        android:pathData="M7,10l5,5 5,-5z"/>
+        android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
 </vector>
diff --git a/car-ui-lib/res/drawable/car_ui_icon_close.xml b/car-ui-lib/res/drawable/car_ui_icon_close.xml
new file mode 100644
index 0000000..482df0f
--- /dev/null
+++ b/car-ui-lib/res/drawable/car_ui_icon_close.xml
@@ -0,0 +1,27 @@
+<?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"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FFF"
+        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
\ No newline at end of file
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_icon_down.xml
similarity index 60%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_icon_down.xml
index c5d298b..dac6001 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_icon_down.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright 2019 The Android Open Source Project
+  ~ 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.
@@ -14,12 +14,12 @@
   ~ 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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+  <path
+      android:pathData="M7.41,8.59L12,13.17l4.59-4.58L18,10l-6,6l-6-6L7.41,8.59z"
+      android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/car-ui-lib/res/drawable/car_ui_icon_overflow_menu.xml b/car-ui-lib/res/drawable/car_ui_icon_overflow_menu.xml
new file mode 100644
index 0000000..6228c3b
--- /dev/null
+++ b/car-ui-lib/res/drawable/car_ui_icon_overflow_menu.xml
@@ -0,0 +1,27 @@
+<?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"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FFF"
+        android:pathData="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
+</vector>
diff --git a/car-ui-lib/res/drawable/car_ui_icon_search.xml b/car-ui-lib/res/drawable/car_ui_icon_search.xml
new file mode 100644
index 0000000..52fbad9
--- /dev/null
+++ b/car-ui-lib/res/drawable/car_ui_icon_search.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT 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"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="48"
+    android:viewportHeight="48">
+
+    <path
+        android:fillColor="@color/car_ui_toolbar_menu_item_icon_color"
+        android:pathData="M31 28h-1.59l-.55-.55C30.82 25.18 32 22.23 32 19c0-7.18-5.82-13-13-13S6 11.82 6
+19s5.82 13 13 13c3.23 0 6.18-1.18 8.45-3.13l.55 .55 V31l10 9.98L40.98 38 31
+28zm-12 0c-4.97 0-9-4.03-9-9s4.03-9 9-9 9 4.03 9 9-4.03 9-9 9z" />
+    <path
+        android:pathData="M0 0h48v48H0z" />
+</vector>
diff --git a/car-ui-lib/res/drawable/car_ui_icon_settings.xml b/car-ui-lib/res/drawable/car_ui_icon_settings.xml
new file mode 100644
index 0000000..ebf8576
--- /dev/null
+++ b/car-ui-lib/res/drawable/car_ui_icon_settings.xml
@@ -0,0 +1,36 @@
+<?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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+
+    <path
+        android:pathData="M0 0h20v20H0V0z" />
+    <path
+        android:fillColor="#fffafafa"
+        android:pathData="M21.4 14.2l-1.94-1.45c.03-.25 .04 -.5 .04 -.76s-.01-.51-.04-.76L21.4 9.8c.42-.31
+.52 -.94 .24 -1.41l-1.6-2.76c-.28-.48-.88-.7-1.36-.5l-2.14 .91
+c-.48-.37-1.01-.68-1.57-.92l-.27-2.2c-.06-.52-.56-.92-1.11-.92h-3.18c-.55 0-1.05
+.4 -1.11 .92 l-.26 2.19c-.57 .24 -1.1 .55 -1.58 .92 l-2.14-.91c-.48-.2-1.08 .02
+-1.36 .5 l-1.6 2.76c-.28 .48 -.18 1.1 .24 1.42l1.94 1.45c-.03 .24 -.04 .49 -.04
+.75 s.01 .51 .04 .76 L2.6 14.2c-.42 .31 -.52 .94 -.24 1.41l1.6 2.76c.28 .48 .88
+.7 1.36 .5 l2.14-.91c.48 .37 1.01 .68 1.57 .92 l.27 2.19c.06 .53 .56 .93 1.11
+.93 h3.18c.55 0 1.04-.4 1.11-.92l.27-2.19c.56-.24 1.09-.55 1.57-.92l2.14 .91
+c.48 .2 1.08-.02 1.36-.5l1.6-2.76c.28-.48 .18 -1.1-.24-1.42zM12 15.5c-1.93
+0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>
+</vector>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_list_header_background.xml
similarity index 60%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_list_header_background.xml
index c5d298b..656b191 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.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 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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@android:color/transparent"/>
+</shape>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_list_item_avatar_icon_outline.xml
similarity index 60%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_list_item_avatar_icon_outline.xml
index c5d298b..f8b63f5 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.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 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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+<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-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_list_item_divider.xml
similarity index 65%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_list_item_divider.xml
index c5d298b..1e229c8 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_list_item_divider.xml
@@ -14,12 +14,9 @@
   ~ 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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <size android:height="@dimen/car_ui_list_item_action_divider_height" />
+    <solid android:color="@color/car_ui_list_item_divider" />
+</shape>
\ No newline at end of file
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_preference_icon_chevron.xml
similarity index 65%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_preference_icon_chevron.xml
index c5d298b..61d594c 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_preference_icon_chevron.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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+
+<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-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_recyclerview_button_ripple_background.xml
similarity index 71%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_recyclerview_button_ripple_background.xml
index c5d298b..0acb196 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_recyclerview_button_ripple_background.xml
@@ -14,12 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
+
+<ripple
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+    android:color="@color/car_ui_ripple_color" />
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_recyclerview_divider.xml
similarity index 60%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_recyclerview_divider.xml
index c5d298b..e1c5163 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_recyclerview_divider.xml
@@ -14,12 +14,12 @@
   ~ 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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+
+<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-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_recyclerview_ic_down.xml
similarity index 65%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_recyclerview_ic_down.xml
index c5d298b..380bf46 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_recyclerview_ic_down.xml
@@ -14,12 +14,13 @@
   ~ 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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M14.83,16.42L24,25.59l9.17,-9.17L36,19.25l-12,12 -12,-12z"
+        android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_recyclerview_ic_up.xml
similarity index 65%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_recyclerview_ic_up.xml
index c5d298b..2eff62f 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_recyclerview_ic_up.xml
@@ -14,12 +14,13 @@
   ~ 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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M14.83,30.83L24,21.66l9.17,9.17L36,28 24,16 12,28z"
+        android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_recyclerview_scrollbar_thumb.xml
similarity index 71%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_recyclerview_scrollbar_thumb.xml
index c5d298b..ec6318a 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_recyclerview_scrollbar_thumb.xml
@@ -14,12 +14,10 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
+
+<shape
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+    android:shape="rectangle">
+    <solid android:color="@color/car_ui_scrollbar_thumb" />
+    <corners android:radius="@dimen/car_ui_scrollbar_thumb_radius"/>
+</shape>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_toolbar_menu_item_divider.xml
similarity index 66%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_toolbar_menu_item_divider.xml
index c5d298b..70f32c5 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_toolbar_menu_item_divider.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Copyright (C) 2019 Google Inc.
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -13,13 +14,11 @@
   ~ WITHOUT 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
+  ~
+ -->
+<shape
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+    android:shape="rectangle">
+    <size
+        android:width="@dimen/car_ui_toolbar_menu_item_margin"/>
+</shape>
diff --git a/car-ui-lib/res/drawable/car_ui_toolbar_menu_item_icon_background.xml b/car-ui-lib/res/drawable/car_ui_toolbar_menu_item_icon_background.xml
new file mode 100644
index 0000000..33594c2
--- /dev/null
+++ b/car-ui-lib/res/drawable/car_ui_toolbar_menu_item_icon_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~
+  ~ Copyright (C) 2019 Google Inc.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="oval">
+    <size
+        android:width="@dimen/car_ui_toolbar_menu_item_icon_background_size"
+        android:height="@dimen/car_ui_toolbar_menu_item_icon_background_size"/>
+    <solid android:color="@color/car_ui_toolbar_menu_item_icon_background_color"/>
+</shape>
+
+
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
similarity index 60%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
index c5d298b..60c485d 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Copyright (C) 2019 Google Inc.
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
@@ -13,13 +14,8 @@
   ~ WITHOUT 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"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+  ~
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+        android:color="@color/car_ui_ripple_color"
+        android:radius="@dimen/car_ui_toolbar_menu_item_icon_ripple_radius"/>
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/res/layout/car_ui_alert_dialog_edit_text.xml b/car-ui-lib/res/layout/car_ui_alert_dialog_edit_text.xml
new file mode 100644
index 0000000..3cc87c1
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_alert_dialog_edit_text.xml
@@ -0,0 +1,27 @@
+<?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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android">
+    <EditText
+              android:id="@+id/textbox"
+              android:layout_width="match_parent"
+              android:layout_height="@dimen/car_ui_dialog_edittext_height"
+              android:layout_marginTop="@dimen/car_ui_dialog_edittext_margin_top"
+              android:layout_marginBottom="@dimen/car_ui_dialog_edittext_margin_bottom"
+              android:layout_marginStart="@dimen/car_ui_dialog_edittext_margin_start"
+              android:layout_marginEnd="@dimen/car_ui_dialog_edittext_margin_end"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/layout/car_ui_alert_dialog_list.xml
similarity index 67%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/layout/car_ui_alert_dialog_list.xml
index c5d298b..34c7dbd 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.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 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/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+    android:id="@+id/list"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+</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
new file mode 100644
index 0000000..271280d
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
@@ -0,0 +1,50 @@
+<?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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/title_template"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    style="@style/Widget.CarUi.AlertDialog.HeaderContainer">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="@dimen/car_ui_dialog_icon_size"
+        android:layout_height="@dimen/car_ui_dialog_icon_size"
+        style="@style/Widget.CarUi.AlertDialog.Icon"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        style="@style/Widget.CarUi.AlertDialog.TitleContainer">
+        <TextView
+            android:id="@+id/alertTitle"
+            android:singleLine="true"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textAlignment="viewStart"
+            style="?android:attr/windowTitleStyle" />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:id="@+id/alertSubtitle"
+            android:textAppearance="@style/TextAppearance.CarUi.AlertDialog.Subtitle"/>
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/layout/car_ui_base_layout.xml
similarity index 69%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/res/layout/car_ui_base_layout.xml
index c5d298b..4cf7e8d 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/layout/car_ui_base_layout.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright 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.
@@ -16,10 +16,7 @@
   -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
+    android:layout_width="match_parent"
+    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_check_box_list_item.xml b/car-ui-lib/res/layout/car_ui_check_box_list_item.xml
new file mode 100644
index 0000000..59f0726
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_check_box_list_item.xml
@@ -0,0 +1,65 @@
+<?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:background="?android:attr/selectableItemBackground"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/car_ui_list_item_check_box_height">
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/car_ui_check_box_start_guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="@dimen/car_ui_list_item_check_box_start_inset" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/car_ui_check_box_end_guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_end="@dimen/car_ui_list_item_check_box_end_inset" />
+
+    <FrameLayout
+        android:id="@+id/check_box_container"
+        android:layout_width="@dimen/car_ui_list_item_check_box_icon_container_width"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="@+id/car_ui_check_box_start_guideline"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <CheckBox
+            android:id="@+id/checkbox"
+            android:clickable="false"
+            android:focusable="false"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center" />
+    </FrameLayout>
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin"
+        android:textAppearance="@style/TextAppearance.CarUi.ListItem"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="@+id/car_ui_check_box_end_guideline"
+        app:layout_constraintStart_toEndOf="@+id/check_box_container"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
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
new file mode 100644
index 0000000..8f07636
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_header_list_item.xml
@@ -0,0 +1,55 @@
+<?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:background="@drawable/car_ui_list_header_background"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/car_ui_list_item_header_height">
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/car_ui_list_item_start_guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="@dimen/car_ui_list_item_header_start_inset" />
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/car_ui_list_item_text_no_icon_start_margin"
+        android:textAppearance="@style/TextAppearance.CarUi.ListItem.Header"
+        app:layout_constraintBottom_toTopOf="@+id/body"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@id/car_ui_list_item_start_guideline"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_chainStyle="packed" />
+
+    <TextView
+        android:id="@+id/body"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/car_ui_list_item_text_no_icon_start_margin"
+        android:textAppearance="@style/TextAppearance.CarUi.ListItem.Body"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@id/car_ui_list_item_start_guideline"
+        app:layout_constraintTop_toBottomOf="@+id/title" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/car-ui-lib/res/layout/car_ui_list_item.xml b/car-ui-lib/res/layout/car_ui_list_item.xml
new file mode 100644
index 0000000..d838fcb
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_list_item.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="@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. -->
+    <View
+        android:id="@+id/touch_interceptor"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="@drawable/car_ui_list_item_background"
+        android:clickable="true"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <!-- This touch interceptor does not include the action container -->
+    <View
+        android:id="@+id/reduced_touch_interceptor"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:background="@drawable/car_ui_list_item_background"
+        android:clickable="true"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/action_container"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/car_ui_list_item_start_guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_begin="@dimen/car_ui_list_item_start_inset" />
+
+    <FrameLayout
+        android:id="@+id/icon_container"
+        android:layout_width="@dimen/car_ui_list_item_icon_container_width"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="@+id/car_ui_list_item_start_guideline"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="@dimen/car_ui_list_item_icon_size"
+            android:layout_height="@dimen/car_ui_list_item_icon_size"
+            android:layout_gravity="center"
+            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>
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin"
+        android:singleLine="@bool/car_ui_list_item_single_line_title"
+        android:textAppearance="@style/TextAppearance.CarUi.ListItem"
+        app:layout_constraintBottom_toTopOf="@+id/body"
+        app:layout_constraintEnd_toStartOf="@+id/action_container"
+        app:layout_constraintStart_toEndOf="@+id/icon_container"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_chainStyle="packed"
+        app:layout_goneMarginStart="@dimen/car_ui_list_item_text_no_icon_start_margin" />
+
+    <TextView
+        android:id="@+id/body"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/car_ui_list_item_text_start_margin"
+        android:textAppearance="@style/TextAppearance.CarUi.ListItem.Body"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/action_container"
+        app:layout_constraintStart_toEndOf="@+id/icon_container"
+        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="wrap_content"
+        android:minWidth="@dimen/car_ui_list_item_icon_container_width"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="@+id/car_ui_list_item_end_guideline"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <View
+            android:id="@+id/action_divider"
+            android:layout_width="@dimen/car_ui_list_item_action_divider_width"
+            android:layout_height="@dimen/car_ui_list_item_action_divider_height"
+            android:layout_gravity="start|center_vertical"
+            android:background="@drawable/car_ui_list_item_divider" />
+
+        <Switch
+            android:id="@+id/switch_widget"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:clickable="false"
+            android:focusable="false" />
+
+        <CheckBox
+            android:id="@+id/checkbox_widget"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:clickable="false"
+            android:focusable="false" />
+
+        <RadioButton
+            android:id="@+id/radio_button_widget"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:clickable="false"
+            android:focusable="false" />
+
+        <ImageView
+            android:id="@+id/supplemental_icon"
+            android:layout_width="@dimen/car_ui_list_item_supplemental_icon_size"
+            android:layout_height="@dimen/car_ui_list_item_supplemental_icon_size"
+            android:layout_gravity="center"
+            android:scaleType="fitXY" />
+    </FrameLayout>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/car_ui_list_item_end_guideline"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_end="@dimen/car_ui_list_item_end_inset" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/car-ui-lib/res/layout/car_ui_list_preference.xml b/car-ui-lib/res/layout/car_ui_list_preference.xml
new file mode 100644
index 0000000..88957b0
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_list_preference.xml
@@ -0,0 +1,37 @@
+<?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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/car_ui_activity_background">
+
+    <com.android.car.ui.recyclerview.CarUiRecyclerView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:enableDivider="true" />
+
+    <com.android.car.ui.toolbar.Toolbar
+        android:id="@+id/toolbar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:state="subpage" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/car-ui-lib/res/layout/car_ui_preference.xml b/car-ui-lib/res/layout/car_ui_preference.xml
new file mode 100644
index 0000000..03e101d
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_preference.xml
@@ -0,0 +1,73 @@
+<?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.
+-->
+
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:clipToPadding="false"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="@dimen/car_ui_preference_icon_size"
+        android:layout_height="@dimen/car_ui_preference_icon_size"
+        android:layout_alignParentStart="true"
+        android:layout_centerVertical="true"
+        android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+        android:layout_marginEnd="@dimen/car_ui_preference_icon_margin_end"
+        android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+        android:scaleType="fitCenter"
+        style="@style/Preference.CarUi.Icon"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+        android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+        android:layout_toEndOf="@android:id/icon"
+        android:layout_toStartOf="@android:id/widget_frame"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"/>
+
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"/>
+
+    </LinearLayout>
+
+    <!-- Preference should place its actual preference widget here. -->
+    <FrameLayout
+        android:id="@android:id/widget_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentEnd="true"
+        android:layout_centerVertical="true"/>
+
+</RelativeLayout>
diff --git a/car-ui-lib/res/layout/car_ui_preference_category.xml b/car-ui-lib/res/layout/car_ui_preference_category.xml
new file mode 100644
index 0000000..a9f3938
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_preference_category.xml
@@ -0,0 +1,64 @@
+<?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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:focusable="true"
+    android:gravity="center_vertical"
+    android:minHeight="@dimen/car_ui_preference_category_min_height"
+    android:orientation="horizontal"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="@dimen/car_ui_preference_category_icon_size"
+        android:layout_height="@dimen/car_ui_preference_category_icon_size"
+        android:layout_gravity="center_vertical"
+        android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+        android:layout_marginEnd="@dimen/car_ui_preference_category_icon_margin_end"
+        android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+        android:scaleType="fitCenter"
+        android:tint="@color/car_ui_preference_icon_color"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+        android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textAlignment="viewStart"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceCategoryTitle"/>
+
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"/>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/car-ui-lib/res/layout/car_ui_preference_chevron.xml b/car-ui-lib/res/layout/car_ui_preference_chevron.xml
new file mode 100644
index 0000000..efeaddb
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_preference_chevron.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.
+-->
+<ImageView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal"
+    android:src="@drawable/car_ui_preference_icon_chevron"/>
\ No newline at end of file
diff --git a/car-ui-lib/res/layout/car_ui_preference_dialog_edittext.xml b/car-ui-lib/res/layout/car_ui_preference_dialog_edittext.xml
new file mode 100644
index 0000000..04c1c37
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_preference_dialog_edittext.xml
@@ -0,0 +1,43 @@
+<?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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginTop="@dimen/car_ui_preference_edit_text_dialog_margin_top"
+    android:layout_marginBottom="@dimen/car_ui_preference_edit_text_dialog_margin_bottom"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@android:id/message"
+        style="@style/TextAppearance.CarUi.PreferenceEditTextDialogMessage"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="@dimen/car_ui_preference_edit_text_dialog_message_margin_bottom"
+        android:layout_marginStart="@dimen/car_ui_preference_edit_text_dialog_message_margin_start"
+        android:layout_marginEnd="@dimen/car_ui_preference_edit_text_dialog_message_margin_end"
+        android:visibility="gone"/>
+
+    <EditText
+        android:id="@android:id/edit"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="@dimen/car_ui_preference_edit_text_dialog_text_margin_end"
+        android:layout_marginStart="@dimen/car_ui_preference_edit_text_dialog_text_margin_start"/>
+
+</LinearLayout>
diff --git a/car-ui-lib/res/layout/car_ui_preference_dropdown.xml b/car-ui-lib/res/layout/car_ui_preference_dropdown.xml
new file mode 100644
index 0000000..cb6e8ce
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_preference_dropdown.xml
@@ -0,0 +1,33 @@
+<?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.
+-->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+    <!-- This spinner should be invisible in the layout and take up no space, when the Preference
+         is clicked the dropdown will appear from this location on screen. -->
+    <Spinner
+        android:id="@+id/spinner"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="@dimen/car_ui_preference_dropdown_padding_start"
+        android:visibility="invisible"/>
+
+    <include layout="@layout/car_ui_preference"/>
+
+</FrameLayout>
diff --git a/car-ui-lib/res/layout/car_ui_preference_fragment.xml b/car-ui-lib/res/layout/car_ui_preference_fragment.xml
new file mode 100644
index 0000000..7298055
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_preference_fragment.xml
@@ -0,0 +1,35 @@
+<?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.
+-->
+
+<FrameLayout
+    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: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>
+</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-media-common/res/anim/media_app_selector_fade_in.xml b/car-ui-lib/res/layout/car_ui_preference_widget_checkbox.xml
similarity index 63%
copy from car-media-common/res/anim/media_app_selector_fade_in.xml
copy to car-ui-lib/res/layout/car_ui_preference_widget_checkbox.xml
index 83e154e..e3f3158 100644
--- a/car-media-common/res/anim/media_app_selector_fade_in.xml
+++ b/car-ui-lib/res/layout/car_ui_preference_widget_checkbox.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright 2017 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,9 +14,12 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 -->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
-    <alpha
-        android:duration="@android:integer/config_mediumAnimTime"
-        android:fromAlpha="0.2"
-        android:toAlpha="1"/>
-</set>
+
+<CheckBox
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/checkbox"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:focusable="false"
+    android:clickable="false"
+    android:background="@null"/>
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
new file mode 100644
index 0000000..e51059f
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_preference_widget_seekbar.xml
@@ -0,0 +1,87 @@
+<?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.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:gravity="center_vertical"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:orientation="horizontal">
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="@dimen/car_ui_preference_icon_size"
+        android:layout_height="@dimen/car_ui_preference_icon_size"
+        android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+        android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom"
+        android:layout_marginEnd="@dimen/car_ui_preference_icon_margin_end"
+        android:scaleType="fitCenter"
+        android:tint="@color/car_ui_preference_icon_color"/>
+
+    <RelativeLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_marginTop="@dimen/car_ui_preference_content_margin_top"
+        android:layout_marginBottom="@dimen/car_ui_preference_content_margin_bottom">
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceTitle"/>
+
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignStart="@android:id/title"
+            android:layout_below="@android:id/title"
+            android:textAppearance="@style/TextAppearance.CarUi.PreferenceSummary"/>
+
+        <!-- Using UnPressableLinearLayout as a workaround to disable the pressed state propagation
+        to the children of this container layout. Otherwise, the animated pressed state will also
+        play for the thumb in the AbsSeekBar in addition to the preference's ripple background.
+        The background of the SeekBar is also set to null to disable the ripple background -->
+        <androidx.preference.UnPressableLinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignStart="@android:id/title"
+            android:layout_below="@android:id/summary"
+            android:clipChildren="false"
+            android:clipToPadding="false">
+            <SeekBar
+                android:id="@+id/seekbar"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                style="@style/Widget.CarUi.SeekbarPreference.Seekbar"/>
+
+            <TextView
+                android:id="@+id/seekbar_value"
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:visibility="gone"/>
+        </androidx.preference.UnPressableLinearLayout>
+
+    </RelativeLayout>
+
+</LinearLayout>
diff --git a/car-media-common/res/anim/media_app_selector_fade_in.xml b/car-ui-lib/res/layout/car_ui_preference_widget_switch.xml
similarity index 65%
copy from car-media-common/res/anim/media_app_selector_fade_in.xml
copy to car-ui-lib/res/layout/car_ui_preference_widget_switch.xml
index 83e154e..2ac924e 100644
--- a/car-media-common/res/anim/media_app_selector_fade_in.xml
+++ b/car-ui-lib/res/layout/car_ui_preference_widget_switch.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright 2017 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,9 +14,11 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 -->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
-    <alpha
-        android:duration="@android:integer/config_mediumAnimTime"
-        android:fromAlpha="0.2"
-        android:toAlpha="1"/>
-</set>
+
+<Switch
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/switch_widget"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:clickable="false"
+    android:focusable="false" />
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-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/res/layout/car_ui_recycler_view_item.xml
similarity index 74%
rename from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
rename to car-ui-lib/res/layout/car_ui_recycler_view_item.xml
index c5d298b..6a35b43 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/res/layout/car_ui_recycler_view_item.xml
@@ -14,12 +14,11 @@
   ~ 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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+    android:id="@+id/nested_recycler_view_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center">
+</FrameLayout>
diff --git a/car-ui-lib/res/layout/car_ui_recyclerview_scrollbar.xml b/car-ui-lib/res/layout/car_ui_recyclerview_scrollbar.xml
new file mode 100644
index 0000000..ceadfaf
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_recyclerview_scrollbar.xml
@@ -0,0 +1,55 @@
+<?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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    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
+        android:id="@+id/page_up"
+        android:layout_width="@dimen/car_ui_scrollbar_button_size"
+        android:layout_height="@dimen/car_ui_scrollbar_button_size"
+        android:background="@drawable/car_ui_recyclerview_button_ripple_background"
+        android:contentDescription="@string/car_ui_scrollbar_page_up_button"
+        android:focusable="false"
+        android:hapticFeedbackEnabled="false"
+        android:src="@drawable/car_ui_recyclerview_ic_up"
+        android:scaleType="centerInside" />
+
+    <!-- View height is dynamically calculated during layout. -->
+    <View
+        android:id="@+id/scrollbar_thumb"
+        android:layout_width="@dimen/car_ui_scrollbar_thumb_width"
+        android:layout_height="0dp"
+        android:layout_gravity="center_horizontal"
+        android:background="@drawable/car_ui_recyclerview_scrollbar_thumb" />
+
+    <ImageButton
+        android:id="@+id/page_down"
+        android:layout_width="@dimen/car_ui_scrollbar_button_size"
+        android:layout_height="@dimen/car_ui_scrollbar_button_size"
+        android:background="@drawable/car_ui_recyclerview_button_ripple_background"
+        android:contentDescription="@string/car_ui_scrollbar_page_down_button"
+        android:focusable="false"
+        android:hapticFeedbackEnabled="false"
+        android:src="@drawable/car_ui_recyclerview_ic_down"
+        android:scaleType="centerInside" />
+</LinearLayout>
diff --git a/car-ui-lib/res/layout/car_ui_toolbar.xml b/car-ui-lib/res/layout/car_ui_toolbar.xml
new file mode 100644
index 0000000..f6ba021
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_toolbar.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2019, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="@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.
+         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"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/car_ui_toolbar_start_guideline"
+        app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_start_inset"
+        android:orientation="vertical"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/car_ui_toolbar_top_guideline"
+        app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_top_inset"
+        android:orientation="horizontal"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/car_ui_toolbar_end_guideline"
+        app:layout_constraintGuide_end="@dimen/car_ui_toolbar_end_inset"
+        android:orientation="vertical"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/car_ui_toolbar_bottom_guideline"
+        app:layout_constraintGuide_end="@dimen/car_ui_toolbar_bottom_inset"
+        android:orientation="horizontal"/>
+
+    <!-- The horizontal bias 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"
+        android:layout_width="@dimen/car_ui_toolbar_margin"
+        android:layout_height="0dp"
+        style="@style/Widget.CarUi.Toolbar.NavIconContainer"
+        app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+        app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_start_guideline"
+        app:layout_constraintHorizontal_bias="0.0">
+        <ImageView
+            android:id="@+id/car_ui_toolbar_nav_icon"
+            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"
+            style="@style/Widget.CarUi.Toolbar.NavIcon"/>
+        <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"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        style="@style/Widget.CarUi.Toolbar.LogoContainer"
+        app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+        app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_nav_icon_container">
+
+        <ImageView
+            android:id="@+id/car_ui_toolbar_title_logo"
+            android:layout_width="@dimen/car_ui_toolbar_logo_size"
+            android:layout_height="@dimen/car_ui_toolbar_logo_size"
+            android:scaleType="fitXY"
+            android:layout_gravity="center"
+            style="@style/Widget.CarUi.Toolbar.Logo"/>
+    </FrameLayout>
+
+    <TextView
+        android:id="@+id/car_ui_toolbar_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        style="@style/Widget.CarUi.Toolbar.Title"
+        app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+        app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_title_logo_container"
+        app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"/>
+
+    <com.android.car.ui.toolbar.TabLayout
+        android:id="@+id/car_ui_toolbar_tabs"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+        app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_title_logo_container"
+        app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"
+        app:layout_constraintHorizontal_bias="0.0"/>
+
+    <LinearLayout
+        android:id="@+id/car_ui_toolbar_menu_items_container"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:orientation="horizontal"
+        style="@style/Widget.CarUi.Toolbar.MenuItem.Container"
+        app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+        app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_end_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_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+        app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_nav_icon_container"
+        app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"/>
+
+    <View
+        android:id="@+id/car_ui_toolbar_row_separator"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/car_ui_toolbar_separator_height"
+        style="@style/Widget.CarUi.Toolbar.SeparatorView"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="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"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:indeterminate="true"
+        android:visibility="gone"/>
+
+    <View
+        android:id="@+id/car_ui_toolbar_bottom_styleable"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/car_ui_toolbar_bottom_view_height"
+        style="@style/Widget.CarUi.Toolbar.BottomView"
+        app:layout_constraintBottom_toTopOf="@+id/car_ui_toolbar_progress_bar"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
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
new file mode 100644
index 0000000..a24b1cf
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_toolbar_menu_item.xml
@@ -0,0 +1,63 @@
+<?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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    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"
+        android:layout_height="match_parent"
+        android:layout_gravity="center">
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:src="@drawable/car_ui_toolbar_menu_item_icon_background"
+            android:background="@drawable/car_ui_toolbar_menu_item_icon_ripple"
+            android:scaleType="center"/>
+        <ImageView
+            android:id="@+id/car_ui_toolbar_menu_item_icon"
+            android:layout_width="@dimen/car_ui_toolbar_menu_item_icon_size"
+            android:layout_height="@dimen/car_ui_toolbar_menu_item_icon_size"
+            android:layout_gravity="center"
+            android:tint="@color/car_ui_toolbar_menu_item_icon_color"
+            android:tintMode="src_in"/>
+    </FrameLayout>
+    <com.android.car.ui.uxr.DrawableStateSwitch
+        android:id="@+id/car_ui_toolbar_menu_item_switch"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:clickable="false"/>
+
+    <!-- These buttons must have clickable="false" or they will steal the click events from the container -->
+    <com.android.car.ui.uxr.DrawableStateButton
+        android:id="@+id/car_ui_toolbar_menu_item_text"
+        style="@style/Widget.CarUi.Toolbar.TextButton"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        android:clickable="false"/>
+    <com.android.car.ui.uxr.DrawableStateButton
+        android:id="@+id/car_ui_toolbar_menu_item_text_with_icon"
+        style="@style/Widget.CarUi.Toolbar.TextButton.WithIcon"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        android:clickable="false"/>
+</FrameLayout>
diff --git a/car-ui-lib/res/layout/car_ui_toolbar_search_view.xml b/car-ui-lib/res/layout/car_ui_toolbar_search_view.xml
new file mode 100644
index 0000000..c3ad68d
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_toolbar_search_view.xml
@@ -0,0 +1,75 @@
+<?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.
+  -->
+
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <EditText
+        android:id="@+id/car_ui_toolbar_search_bar"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent"
+        android:hint="@string/car_ui_toolbar_default_search_hint"
+        android:textColorHint="@color/car_ui_toolbar_search_hint_text_color"
+        android:inputType="text"
+        android:singleLine="true"
+        android:imeOptions="actionSearch"
+        style="@style/Widget.CarUi.Toolbar.Search.EditText"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+    <!-- This constraintLayout is to provide a background for the ripples to draw on, so
+         they don't get drawn underneath the EditText's background -->
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@android:color/transparent">
+        <FrameLayout
+            android:layout_width="@dimen/car_ui_toolbar_search_search_icon_container_width"
+            android:layout_height="match_parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent">
+            <ImageView
+                android:id="@+id/car_ui_toolbar_search_icon"
+                android:layout_width="@dimen/car_ui_toolbar_search_search_icon_size"
+                android:layout_height="@dimen/car_ui_toolbar_search_search_icon_size"
+                android:layout_gravity="center"
+                android:src="@drawable/car_ui_toolbar_search_search_icon"
+                android:scaleType="fitXY"
+                style="@style/Widget.CarUi.Toolbar.Search.SearchIcon"/>
+        </FrameLayout>
+
+        <FrameLayout
+            android:id="@+id/car_ui_toolbar_search_close"
+            android:layout_width="@dimen/car_ui_toolbar_search_close_icon_container_width"
+            android:layout_height="match_parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent">
+            <ImageView
+                android:layout_width="@dimen/car_ui_toolbar_search_close_icon_size"
+                android:layout_height="@dimen/car_ui_toolbar_search_close_icon_size"
+                android:layout_gravity="center"
+                android:src="@drawable/car_ui_toolbar_search_close_icon"
+                android:scaleType="fitXY"
+                style="@style/Widget.CarUi.Toolbar.Search.CloseIcon"/>
+        </FrameLayout>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</merge>
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
new file mode 100644
index 0000000..9172f5b
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_toolbar_tab_item.xml
@@ -0,0 +1,33 @@
+<?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="wrap_content"
+    android:layout_height="match_parent"
+    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_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
new file mode 100644
index 0000000..428a2b6
--- /dev/null
+++ b/car-ui-lib/res/layout/car_ui_toolbar_two_row.xml
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2019, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:id="@+id/car_ui_toolbar_background"
+    style="@style/Widget.CarUi.Toolbar.Container">
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/car_ui_toolbar_start_guideline"
+        app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_start_inset"
+        android:orientation="vertical"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/car_ui_toolbar_top_guideline"
+        app:layout_constraintGuide_begin="@dimen/car_ui_toolbar_top_inset"
+        android:orientation="horizontal"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/car_ui_toolbar_end_guideline"
+        app:layout_constraintGuide_end="@dimen/car_ui_toolbar_end_inset"
+        android:orientation="vertical"/>
+
+    <androidx.constraintlayout.widget.Guideline
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:id="@+id/car_ui_toolbar_bottom_guideline"
+        app:layout_constraintGuide_end="@dimen/car_ui_toolbar_bottom_inset"
+        android:orientation="horizontal"/>
+
+    <!-- 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"/>
+
+    <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"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/car_ui_toolbar_separator_height"
+        style="@style/Widget.CarUi.Toolbar.SeparatorView"
+        app:layout_constraintTop_toBottomOf="@id/car_ui_toolbar_row_separator_guideline"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"/>
+
+    <FrameLayout
+        android:id="@+id/car_ui_toolbar_nav_icon_container"
+        android:layout_width="@dimen/car_ui_toolbar_margin"
+        android:layout_height="0dp"
+        style="@style/Widget.CarUi.Toolbar.NavIconContainer"
+        app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+        app:layout_constraintStart_toStartOf="@id/car_ui_toolbar_start_guideline">
+        <ImageView
+            android:id="@+id/car_ui_toolbar_nav_icon"
+            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"
+            style="@style/Widget.CarUi.Toolbar.NavIcon"/>
+        <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"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        style="@style/Widget.CarUi.Toolbar.LogoContainer"
+        app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+        app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_nav_icon_container">
+
+        <ImageView
+            android:id="@+id/car_ui_toolbar_title_logo"
+            android:layout_width="@dimen/car_ui_toolbar_logo_size"
+            android:layout_height="@dimen/car_ui_toolbar_logo_size"
+            android:scaleType="fitXY"
+            android:layout_gravity="center"
+            style="@style/Widget.CarUi.Toolbar.Logo"/>
+    </FrameLayout>
+
+    <TextView
+        android:id="@+id/car_ui_toolbar_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:singleLine="true"
+        style="@style/Widget.CarUi.Toolbar.Title"
+        app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+        app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_title_logo_container"
+        app:layout_constraintEnd_toStartOf="@id/car_ui_toolbar_menu_items_container"/>
+
+    <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_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+        app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_nav_icon_container"
+        app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"/>
+
+    <LinearLayout
+        android:id="@+id/car_ui_toolbar_menu_items_container"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:orientation="horizontal"
+        style="@style/Widget.CarUi.Toolbar.MenuItem.Container"
+        app:layout_constraintTop_toTopOf="@id/car_ui_toolbar_top_guideline"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_row_separator"
+        app:layout_constraintEnd_toStartOf="@id/car_ui_toolbar_end_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_constraintTop_toBottomOf="@id/car_ui_toolbar_row_separator"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_guideline"/>
+
+    <View
+        android:id="@+id/car_ui_toolbar_bottom_styleable"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/car_ui_toolbar_bottom_view_height"
+        style="@style/Widget.CarUi.Toolbar.BottomView"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="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"
+        app:layout_constraintBottom_toTopOf="@id/car_ui_toolbar_bottom_styleable"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:indeterminate="true"
+        android:visibility="gone"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-ui-lib/res/values-port/bools.xml b/car-ui-lib/res/values-port/bools.xml
new file mode 100644
index 0000000..16f3724
--- /dev/null
+++ b/car-ui-lib/res/values-port/bools.xml
@@ -0,0 +1,26 @@
+<?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>
+    <!-- Toolbar -->
+
+    <!-- Whether tabs should use flex layout or not -->
+    <bool name="car_ui_toolbar_tab_flexible_layout">true</bool>
+    <!-- Whether tabs should be displayed on a second row, or they should be placed in the first
+         row, replacing the title -->
+    <bool name="car_ui_toolbar_tabs_on_second_row">true</bool>
+</resources>
diff --git a/car-ui-lib/res/values-w1280dp/dimens.xml b/car-ui-lib/res/values-w1280dp/dimens.xml
new file mode 100644
index 0000000..a06df2b
--- /dev/null
+++ b/car-ui-lib/res/values-w1280dp/dimens.xml
@@ -0,0 +1,22 @@
+<?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">
+    <!-- Keylines -->
+    <dimen name="car_ui_keyline_1">32dp</dimen>
+    <dimen name="car_ui_keyline_2">108dp</dimen>
+    <dimen name="car_ui_keyline_3">128dp</dimen>
+    <dimen name="car_ui_keyline_4">168dp</dimen>
+</resources>
diff --git a/car-ui-lib/res/values/attrs.xml b/car-ui-lib/res/values/attrs.xml
new file mode 100644
index 0000000..8f7b3e0
--- /dev/null
+++ b/car-ui-lib/res/values/attrs.xml
@@ -0,0 +1,138 @@
+<?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>
+    <!-- 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"/>
+        <!-- Logo drawable for the toolbar. Appears when there's no back/close button shown -->
+        <attr name="logo" format="reference"/>
+        <!-- Hint for the search bar in the toolbar -->
+        <attr name="searchHint" format="string"/>
+        <!-- Whether or not to show the MenuItems while searching. Default false. -->
+        <attr name="showMenuItemsWhileSearching" format="boolean"/>
+        <!-- Initial state of the toolbar. See the Toolbar.State enum for more information -->
+        <attr name="state" format="enum">
+            <enum name="home" value="0"/>
+            <enum name="subpage" value="1"/>
+            <enum name="search" value="2"/>
+        </attr>
+        <!-- Whether or not the toolbar should have a background. Default true. -->
+        <attr name="showBackground" format="boolean"/>
+        <!-- Mode of the navigation button See the Toolbar.NavButtonMode enum for more information -->
+        <attr name="navButtonMode" format="enum">
+            <enum name="back" value="0"/>
+            <enum name="close" value="1"/>
+            <enum name="down" value="2"/>
+        </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">
+        <!-- Id of MenuItem, used to differentiate them -->
+        <attr name="id" format="reference"/>
+        <!-- Show/hide the MenuItem -->
+        <attr name="visible" format="boolean"/>
+        <!-- Set this to true to make a search MenuItem. This will override every other property except id, visible, and onclick. -->
+        <attr name="search" format="boolean"/>
+        <!-- Set this to true to make a settings MenuItem. This will override every other property except id, visible, and onclick. -->
+        <attr name="settings" format="boolean"/>
+        <!-- Title -->
+        <attr name="title"/>
+        <!-- Icon -->
+        <attr name="icon" format="reference"/>
+        <!-- True to tint the icon to a consistent color. Default true, all the other booleans default to false -->
+        <attr name="tinted" format="boolean"/>
+        <!-- Show both the icon and title at the same time -->
+        <attr name="showIconAndTitle" format="boolean"/>
+        <!-- True if this MenuItem should be a switch -->
+        <attr name="checkable" format="boolean"/>
+        <!-- Whether the switch should be checked or not. Setting this implies checkable=true -->
+        <attr name="checked" format="boolean"/>
+        <!-- True if this MenuItem should be activatable, in which case it will visually toggle states when clicked -->
+        <attr name="activatable" format="boolean"/>
+        <!-- Whether the MenuItem starts activated. Setting this implies activatable=true -->
+        <attr name="activated" format="boolean"/>
+        <!-- How to display the MenuItem. "always" means always show it on the toolbar, "never" means never show it on the toolbar and instead show it in the overflow menu -->
+        <attr name="displayBehavior" format="enum">
+            <enum name="always" value="0"/>
+            <enum name="never" value="1"/>
+        </attr>
+        <!-- Ux restrictions required to interact with this MenuItem -->
+        <attr name="uxRestrictions">
+            <!-- Values are copied from android.car.drivingstate.CarUxRestrictions. Note:
+            UX_RESTRICTIONS_BASELINE is not allowed here because it's useless and confusing. -->
+            <flag name="UX_RESTRICTIONS_NO_DIALPAD" value="1"/>
+            <flag name="UX_RESTRICTIONS_NO_FILTERING" value="2"/>
+            <flag name="UX_RESTRICTIONS_LIMIT_STRING_LENGTH" value="4"/>
+            <flag name="UX_RESTRICTIONS_NO_KEYBOARD" value="8"/>
+            <flag name="UX_RESTRICTIONS_NO_VIDEO" value="16"/>
+            <flag name="UX_RESTRICTIONS_LIMIT_CONTENT" value="32"/>
+            <flag name="UX_RESTRICTIONS_NO_SETUP" value="64"/>
+            <flag name="UX_RESTRICTIONS_NO_TEXT_MESSAGE" value="128"/>
+            <flag name="UX_RESTRICTIONS_NO_VOICE_TRANSCRIPTION" value="256"/>
+            <flag name="UX_RESTRICTIONS_FULLY_RESTRICTED" value="511"/>
+        </attr>
+        <!-- The name of a method that takes a MenuItem as an argument in you'r toolbar's Activity. Will be called when the MenuItem is clicked -->
+        <attr name="onClick" format="string"/>
+    </declare-styleable>
+
+    <!-- Theme attribute to specifying a default style for all CarUiToolbars -->
+    <attr name="CarUiToolbarStyle" format="reference"/>
+
+    <declare-styleable name="CarUiRecyclerView">
+        <!-- Whether to enable the car_ui_recyclerview_divider for linear layout or not. -->
+        <attr name="enableDivider" format="boolean" />
+        <!-- Top offset for car ui recycler view. -->
+        <attr name="startOffset" format="integer" />
+        <!-- Bottom offset for car ui recycler view for linear layout. -->
+        <attr name="endOffset" format="integer" />
+
+        <!-- Number of columns in a grid layout. -->
+        <attr name="numOfColumns" format="integer" />
+
+        <!-- car ui recycler view layout. -->
+        <attr name="layoutStyle" format="enum">
+            <!-- linear layout -->
+            <enum name="linear" value="0" />
+            <!-- grid layout -->
+            <enum name="grid" value="1" />
+        </attr>
+    </declare-styleable>
+
+    <declare-styleable name="CarUiPreference">
+        <!-- Toggle for showing chevron -->
+        <attr name="showChevron" format="boolean" />
+    </declare-styleable>
+
+    <!-- Theme attribute to specify a default style for all CarUiPreferences -->
+    <attr name="carUiPreferenceStyle" format="reference" />
+
+    <!-- Theme attribute to specify a default style for all CarUiRecyclerViews -->
+    <attr name="carUiRecyclerViewStyle" format="reference" />
+
+    <attr name="state_ux_restricted" format="boolean" />
+</resources>
diff --git a/car-ui-lib/res/values/bools.xml b/car-ui-lib/res/values/bools.xml
new file mode 100644
index 0000000..955956d
--- /dev/null
+++ b/car-ui-lib/res/values/bools.xml
@@ -0,0 +1,49 @@
+<?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>
+    <!-- Toolbar -->
+
+    <!-- Whether tabs should use flex layout or not -->
+    <bool name="car_ui_toolbar_tab_flexible_layout">false</bool>
+    <!-- Whether the space for nav icon should be reserved, even if the nav icon is not visible -->
+    <bool name="car_ui_toolbar_nav_icon_reserve_space">true</bool>
+    <!-- Whether the logo (if provided) should be used in place of the nav icon when nav icon is
+         not visible -->
+    <bool name="car_ui_toolbar_logo_fills_nav_icon_space">true</bool>
+    <!-- Whether logo should be displayed. If set to false, logo won't be shown even if provided -->
+    <bool name="car_ui_toolbar_show_logo">true</bool>
+    <!-- Whether tabs should be displayed on a second row, or they should be placed in the first
+         row, replacing the title -->
+    <bool name="car_ui_toolbar_tabs_on_second_row">false</bool>
+
+    <!-- CarUiRecyclerView -->
+
+    <!-- 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>
+
+    <!-- Preferences -->
+
+    <!-- Whether list, edit, dropdown and intent preferences should show a chevron or not -->
+    <bool name="car_ui_preference_show_chevron">false</bool>
+    <!-- whether list preference should be shown in full screen or as a dialog -->
+    <bool name="car_ui_preference_list_show_full_screen">true</bool>
+
+    <!-- List items -->
+
+    <bool name="car_ui_list_item_single_line_title">true</bool>
+</resources>
diff --git a/car-ui-lib/res/values/colors.xml b/car-ui-lib/res/values/colors.xml
new file mode 100644
index 0000000..78d1ecc
--- /dev/null
+++ b/car-ui-lib/res/values/colors.xml
@@ -0,0 +1,55 @@
+<?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>
+    <!-- General -->
+
+    <!-- Background color to use on full screen activities -->
+    <color name="car_ui_activity_background_color">#000000</color>
+    <!-- The ripple color. -->
+    <color name="car_ui_ripple_color">#27ffffff</color>
+
+    <!-- Toolbar -->
+
+    <!-- Color used on the navigation icon -->
+    <color name="car_ui_toolbar_nav_icon_color">@color/car_ui_text_color_primary</color>
+    <!-- Text color applied to the hint displayed inside the search box -->
+    <color name="car_ui_toolbar_search_hint_text_color">@color/car_ui_text_color_hint</color>
+    <!-- Tab selected color -->
+    <color name="car_ui_toolbar_tab_selected_color">@color/car_ui_text_color_primary</color>
+    <!-- Tab normal color -->
+    <color name="car_ui_toolbar_tab_unselected_color">@color/car_ui_text_color_secondary</color>
+
+    <!-- Recycler View  -->
+
+    <!-- Color of the scroll bar indicator in the CarUiRecyclerView. -->
+    <color name="car_ui_scrollbar_thumb">#99ffffff</color>
+    <!-- Color of the divider views between CarUiRecyclerView items -->
+    <color name="car_ui_recyclerview_divider_color">@android:color/transparent</color>
+
+    <!-- Preferences -->
+
+    <color name="car_ui_preference_category_title_text_color">@color/car_ui_color_accent</color>
+    <color name="car_ui_preference_summary_text_color">@color/car_ui_text_color_secondary</color>
+    <color name="car_ui_preference_title_text_color">@color/car_ui_text_color_primary</color>
+    <color name="car_ui_preference_edit_text_dialog_message_text_color">@color/car_ui_text_color_primary</color>
+    <color name="car_ui_preference_icon_color">@color/car_ui_text_color_primary</color>
+    <color name="car_ui_preference_switch_track_text_color">@color/car_ui_text_color_primary</color>
+
+    <!-- List item -->
+    <color name="car_ui_list_item_header_text_color">@color/car_ui_color_accent</color>
+    <color name="car_ui_list_item_title_text_color">@color/car_ui_text_color_primary</color>
+    <color name="car_ui_list_item_body_text_color">@color/car_ui_text_color_secondary</color>
+</resources>
diff --git a/car-ui-lib/res/values/dimens.xml b/car-ui-lib/res/values/dimens.xml
new file mode 100644
index 0000000..3a77194
--- /dev/null
+++ b/car-ui-lib/res/values/dimens.xml
@@ -0,0 +1,220 @@
+<?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>
+    <!-- General resources -->
+
+    <dimen name="car_ui_touch_target_width">76dp</dimen>
+    <dimen name="car_ui_touch_target_height">76dp</dimen>
+    <dimen name="car_ui_primary_icon_size">44dp</dimen>
+    <item name="car_ui_letter_spacing_body1" format="float" type="dimen">0.0</item>
+    <item name="car_ui_letter_spacing_body3" format="float" type="dimen">0.0</item>
+
+    <!-- Horizontal margin between screen content and display border. In reference
+     implementation, this value matches the CarUiRecyclerView scrollbar width -->
+    <dimen name="car_ui_margin">112dp</dimen>
+
+    <!-- Paddings -->
+    <dimen name="car_ui_padding_0">4dp</dimen>
+    <dimen name="car_ui_padding_1">8dp</dimen>
+    <dimen name="car_ui_padding_2">16dp</dimen>
+    <dimen name="car_ui_padding_3">24dp</dimen>
+    <dimen name="car_ui_padding_4">32dp</dimen>
+    <dimen name="car_ui_padding_5">64dp</dimen>
+    <dimen name="car_ui_padding_6">96dp</dimen>
+
+    <!-- Type Sizings -->
+    <dimen name="car_ui_body1_size">32sp</dimen>
+    <dimen name="car_ui_body2_size">28sp</dimen>
+    <dimen name="car_ui_body3_size">24sp</dimen>
+
+    <!-- Keylines -->
+    <dimen name="car_ui_keyline_1">24dp</dimen>
+    <dimen name="car_ui_keyline_2">96dp</dimen>
+    <dimen name="car_ui_keyline_3">112dp</dimen>
+    <dimen name="car_ui_keyline_4">148dp</dimen>
+
+    <!-- Tabs -->
+
+    <!-- Exact size of the tab textbox. Use @dimen/wrap_content if this must be flexible -->
+    <dimen name="car_ui_toolbar_tab_text_width">135dp</dimen>
+    <!-- Horizontal padding between tabs -->
+    <dimen name="car_ui_toolbar_tab_padding_x">12dp</dimen>
+    <!-- Tab icon width (if icons are enabled) -->
+    <dimen name="car_ui_toolbar_tab_icon_width">36dp</dimen>
+    <!-- Tab icon height (if icons are enabled) -->
+    <dimen name="car_ui_toolbar_tab_icon_height">36dp</dimen>
+
+    <!-- Toolbar -->
+
+    <!-- Default height for both toolbar rows. See car_ui_toolbar_first_row_height and
+     car_ui_toolbar_second_row_height -->
+    <dimen name="car_ui_toolbar_row_height">96dp</dimen>
+    <!-- Height of the top toolbar row. This can be customized independently. -->
+    <dimen name="car_ui_toolbar_first_row_height">@dimen/car_ui_toolbar_row_height</dimen>
+    <!-- Height of the bottom toolbar row (if the toolbar is used in two-rows mode. -->
+    <dimen name="car_ui_toolbar_second_row_height">@dimen/car_ui_toolbar_row_height</dimen>
+    <!-- Padding on the toolbar start (e.g.: distance between the container start and the start of
+    nav icon or logo) -->
+    <dimen name="car_ui_toolbar_start_inset">0dp</dimen>
+    <!-- End padding (e.g.: distance between the container end and the end of the menu items) -->
+    <dimen name="car_ui_toolbar_end_inset">0dp</dimen>
+    <!-- Top padding -->
+    <dimen name="car_ui_toolbar_top_inset">0dp</dimen>
+    <!-- Bottom padding -->
+    <dimen name="car_ui_toolbar_bottom_inset">0dp</dimen>
+    <!-- Toolbar title/tabs start margin. Toolbar navigation icon (or logo if no navigation icon is
+    used) will be centered in this space, and the title will start from here -->
+    <dimen name="car_ui_toolbar_margin">@dimen/car_ui_margin</dimen>
+    <!-- Navigation icon -->
+    <dimen name="car_ui_toolbar_nav_icon_size">@dimen/car_ui_primary_icon_size</dimen>
+    <!-- Logo -->
+    <dimen name="car_ui_toolbar_logo_size">@dimen/car_ui_primary_icon_size</dimen>
+    <!-- Margin between the logo and the title, when both logo and navigation icons are used -->
+    <dimen name="car_ui_toolbar_title_logo_padding">0dp</dimen>
+    <!-- Margin at the start of the title -->
+    <dimen name="car_ui_toolbar_title_margin_start">@dimen/car_ui_padding_2</dimen>
+    <!-- Space at the end and in between menu items -->
+    <dimen name="car_ui_toolbar_menu_item_margin">@dimen/car_ui_padding_2</dimen>
+    <!-- Ripple effect radius for icon menu items -->
+    <dimen name="car_ui_toolbar_menu_item_icon_ripple_radius">48dp</dimen>
+    <!-- Icon size for icon menu items -->
+    <dimen name="car_ui_toolbar_menu_item_icon_size">@dimen/car_ui_primary_icon_size</dimen>
+    <!-- Icon background size for icon menu items -->
+    <dimen name="car_ui_toolbar_menu_item_icon_background_size">54dp</dimen>
+    <!-- Height of the decoration view between the two rows of the toolbar (or below the toolbar
+    if this is a single row one -->
+    <!-- can't use 0dp for layout_height or the constraintlayout effect kicks in -->
+    <dimen name="car_ui_toolbar_separator_height">0.1dp</dimen>
+    <!-- Height of the decoration view below the toolbar -->
+    <!-- can't use 0dp for layout_height or the constraintlayout effect kicks in -->
+    <dimen name="car_ui_toolbar_bottom_view_height">0.1dp</dimen>
+    <!-- Height of the search box -->
+    <dimen name="car_ui_toolbar_search_height">0dp</dimen>
+    <!-- Space before the text search area, where the search icon is located -->
+    <dimen name="car_ui_toolbar_search_search_icon_container_width">@dimen/car_ui_touch_target_width</dimen>
+    <!-- Space after the text search area, where the cancel icon is located -->
+    <dimen name="car_ui_toolbar_search_close_icon_container_width">@dimen/car_ui_touch_target_width</dimen>
+    <!-- Size of the search icon inside the search box -->
+    <dimen name="car_ui_toolbar_search_search_icon_size">@dimen/car_ui_primary_icon_size</dimen>
+    <!-- Size of the close icon inside the search box -->
+    <dimen name="car_ui_toolbar_search_close_icon_size">@dimen/car_ui_primary_icon_size</dimen>
+
+    <!-- Internal artifacts. Do not overlay -->
+    <item name="wrap_content" format="integer" type="dimen">-2</item>
+
+    <!-- CarUiRecyclerView -->
+
+    <dimen name="car_ui_recyclerview_divider_height">0dp</dimen>
+    <dimen name="car_ui_recyclerview_divider_start_margin">0dp</dimen>
+    <dimen name="car_ui_recyclerview_divider_end_margin">0dp</dimen>
+    <dimen name="car_ui_recyclerview_divider_top_margin">0dp</dimen>
+    <dimen name="car_ui_recyclerview_divider_bottom_margin">0dp</dimen>
+
+    <!-- CarUiRecyclerView default scrollbar -->
+
+    <dimen name="car_ui_scrollbar_container_width">@dimen/car_ui_margin</dimen>
+    <dimen name="car_ui_scrollbar_button_size">@dimen/car_ui_touch_target_width</dimen>
+    <dimen name="car_ui_scrollbar_thumb_width">7dp</dimen>
+    <dimen name="car_ui_scrollbar_separator_margin">16dp</dimen>
+    <dimen name="car_ui_scrollbar_margin">@dimen/car_ui_margin</dimen>
+    <dimen name="car_ui_scrollbar_thumb_radius">100dp</dimen>
+
+    <item name="car_ui_button_disabled_alpha" format="float" type="dimen">0.2</item>
+    <item name="car_ui_scrollbar_milliseconds_per_inch" format="float" type="dimen">150.0</item>
+    <item name="car_ui_scrollbar_deceleration_times_divisor" format="float" type="dimen">0.45</item>
+    <item name="car_ui_scrollbar_decelerate_interpolator_factor" format="float" type="dimen">1.8</item>
+
+    <dimen name="car_ui_scrollbar_padding_start">0dp</dimen>
+    <dimen name="car_ui_scrollbar_padding_end">0dp</dimen>
+
+    <!-- Preferences -->
+
+    <dimen name="car_ui_preference_category_text_size">24sp</dimen>
+    <dimen name="car_ui_preference_summary_text_size">24sp</dimen>
+    <dimen name="car_ui_preference_title_text_size">32sp</dimen>
+    <dimen name="car_ui_preference_edit_text_dialog_message_text_size">24sp</dimen>
+
+    <dimen name="car_ui_preference_content_margin_top">16dp</dimen>
+    <dimen name="car_ui_preference_content_margin_bottom">16dp</dimen>
+    <dimen name="car_ui_preference_icon_size">44dp</dimen>
+    <dimen name="car_ui_preference_icon_margin_end">16dp</dimen>
+
+    <dimen name="car_ui_preference_category_min_height">76dp</dimen>
+    <dimen name="car_ui_preference_category_icon_size">44dp</dimen>
+    <dimen name="car_ui_preference_category_icon_margin_end">16dp</dimen>
+
+    <dimen name="car_ui_preference_dropdown_padding_start">112dp</dimen>
+
+    <dimen name="car_ui_preference_edit_text_dialog_margin_top">32dp</dimen>
+    <dimen name="car_ui_preference_edit_text_dialog_margin_bottom">32dp</dimen>
+    <dimen name="car_ui_preference_edit_text_dialog_message_margin_bottom">32dp</dimen>
+    <dimen name="car_ui_preference_edit_text_dialog_message_margin_start">24dp</dimen>
+    <dimen name="car_ui_preference_edit_text_dialog_message_margin_end">24dp</dimen>
+    <dimen name="car_ui_preference_edit_text_dialog_text_margin_start">24dp</dimen>
+    <dimen name="car_ui_preference_edit_text_dialog_text_margin_end">24dp</dimen>
+
+    <dimen name="car_ui_preference_switch_text_size">30sp</dimen>
+    <dimen name="car_ui_preference_switch_width">288dp</dimen>
+    <dimen name="car_ui_preference_switch_width_half">144dp</dimen>
+    <dimen name="car_ui_preference_switch_height">101dp</dimen>
+
+    <!-- Alert dialog   -->
+
+    <dimen name="car_ui_dialog_edittext_height">50dp</dimen>
+    <dimen name="car_ui_dialog_edittext_margin_top">10dp</dimen>
+    <dimen name="car_ui_dialog_edittext_margin_bottom">10dp</dimen>
+    <dimen name="car_ui_dialog_edittext_margin_start">22dp</dimen>
+    <dimen name="car_ui_dialog_edittext_margin_end">22dp</dimen>
+    <dimen name="car_ui_dialog_icon_size">56dp</dimen>
+    <dimen name="car_ui_dialog_title_margin">@dimen/car_ui_keyline_1</dimen>
+
+    <!-- List item  -->
+
+    <dimen name="car_ui_list_item_header_text_size">24sp</dimen>
+    <dimen name="car_ui_list_item_title_text_size">32sp</dimen>
+    <dimen name="car_ui_list_item_body_text_size">24sp</dimen>
+    <dimen name="car_ui_list_item_height">116dp</dimen>
+    <dimen name="car_ui_list_item_header_height">76dp</dimen>
+    <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">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>
+    <dimen name="car_ui_list_item_check_box_icon_container_width">@dimen/car_ui_list_item_icon_container_width</dimen>
+
+</resources>
diff --git a/car-ui-lib/res/values/drawables.xml b/car-ui-lib/res/values/drawables.xml
new file mode 100644
index 0000000..181846c
--- /dev/null
+++ b/car-ui-lib/res/values/drawables.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>
+    <!-- General -->
+
+    <!-- Background drawable to use on full screen activities -->
+    <drawable name="car_ui_activity_background">@color/car_ui_activity_background_color</drawable>
+
+    <!-- Toolbar -->
+
+    <!-- Toolbar background color -->
+    <drawable name="car_ui_toolbar_background">#E0000000</drawable>
+    <!-- Search icon shown inside the search box in the toolbar -->
+    <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 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/ids.xml b/car-ui-lib/res/values/ids.xml
new file mode 100644
index 0000000..fcff109
--- /dev/null
+++ b/car-ui-lib/res/values/ids.xml
@@ -0,0 +1,19 @@
+<?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>
+    <!-- Id used for the search button when using Toolbar.createSearch() method -->
+    <item name="search" type="id"/>
+</resources>
\ No newline at end of file
diff --git a/car-ui-lib/res/values/integers.xml b/car-ui-lib/res/values/integers.xml
new file mode 100644
index 0000000..623ef00
--- /dev/null
+++ b/car-ui-lib/res/values/integers.xml
@@ -0,0 +1,20 @@
+<?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>
+    <!-- Default max string length -->
+    <integer name="car_ui_default_max_string_length">120</integer>
+</resources>
\ No newline at end of file
diff --git a/car-ui-lib/res/values/strings.xml b/car-ui-lib/res/values/strings.xml
new file mode 100644
index 0000000..a42068f
--- /dev/null
+++ b/car-ui-lib/res/values/strings.xml
@@ -0,0 +1,58 @@
+<?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>
+    <!--
+    Configuration for a default scrollbar for the CarUiRecyclerView. This component must inherit
+    abstract class ScrollBar. If the ScrollBar is enabled, the component will be initialized from
+    CarUiRecyclerView#createScrollBarFromConfig(). If no component is provided,
+    {@link DefaultScrollbar} class will be used.
+    -->
+    <string name="car_ui_scrollbar_component" translatable="false"/>
+    <!-- Search hint, displayed inside the search box [CHAR LIMIT=50] -->
+    <string name="car_ui_toolbar_default_search_hint">Search&#8230;</string>
+    <!-- CarUxRestrictions Utility -->
+    <string name="car_ui_ellipsis" translatable="false">&#8230;</string>
+    <!-- Content description for car ui recycler view scroll bar down arrow [CHAR LIMIT=30] -->
+    <string name="car_ui_scrollbar_page_down_button">Scroll down</string>
+    <!-- Content description for car ui recycler view scroll bar up arrow [CHAR LIMIT=30] -->
+    <string name="car_ui_scrollbar_page_up_button">Scroll up</string>
+    <!-- Title of the search menu item. Will be displayed if the button is in the overflow menu. [CHAR_LIMIT=50] -->
+    <string name="car_ui_toolbar_menu_item_search_title">Search</string>
+    <!-- Title of the settings menu item. Will be displayed if the button is in the overflow menu. [CHAR_LIMIT=50] -->
+    <string name="car_ui_toolbar_menu_item_settings_title">Settings</string>
+    <!-- Title of the overflow menu item. Only used for content descriptions. [CHAR_LIMIT=50] -->
+    <string name="car_ui_toolbar_menu_item_overflow_title">Overflow</string>
+
+    <!-- Positive option for a preference dialog. [CHAR_LIMIT=30] -->
+    <string name="car_ui_dialog_preference_positive">Ok</string>
+    <!-- Negative option for a preference dialog. [CHAR_LIMIT=30] -->
+    <string name="car_ui_dialog_preference_negative">Cancel</string>
+    <!-- Text to show when a preference switch is on. [CHAR_LIMIT=30] -->
+    <string name="car_ui_preference_switch_on">On</string>
+    <!-- Text to show when a preference switch is off. [CHAR_LIMIT=30] -->
+    <string name="car_ui_preference_switch_off">Off</string>
+    <!-- Font family to use for preference category titles. [CHAR_LIMIT=NONE] -->
+    <string name="car_ui_preference_category_title_font_family" translatable="false">sans-serif-medium</string>
+
+    <!-- Font family to use for list item headers. [CHAR_LIMIT=NONE] -->
+    <string name="car_ui_list_item_header_font_family" translatable="false">sans-serif-medium</string>
+
+    <!-- Text to show when no button is provided and a default button is used. -->
+    <string name="car_ui_alert_dialog_default_button" translatable="false">Close</string>
+
+    <!-- Shown in a toast when the user attempts to do something distracting while driving [CHAR_LIMIT=200] -->
+    <string name="car_ui_restricted_while_driving">Feature not available while driving</string>
+</resources>
diff --git a/car-ui-lib/res/values/styles.xml b/car-ui-lib/res/values/styles.xml
new file mode 100644
index 0000000..9cbf41e
--- /dev/null
+++ b/car-ui-lib/res/values/styles.xml
@@ -0,0 +1,308 @@
+<?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">
+
+    <!-- Styles for CarUi tab view -->
+
+    <style name="Widget.CarUi" parent="android:Widget.DeviceDefault"/>
+
+    <style name="Widget.CarUi.Button.Borderless.Colored"
+           parent="android:Widget.DeviceDefault.Button.Borderless.Colored"/>
+
+    <style name="Widget.CarUi.Button" parent="android:Widget.DeviceDefault.Button"/>
+
+    <style name="Widget.CarUi.Toolbar"/>
+
+    <style name="Widget.CarUi.SeekbarPreference"/>
+
+    <style name="Widget.CarUi.Toolbar.Container"/>
+
+    <style name="Widget.CarUi.Toolbar.NavIconContainer"/>
+
+    <style name="Widget.CarUi.Toolbar.Logo"/>
+
+    <style name="Widget.CarUi.Toolbar.LogoContainer">
+        <item name="android:paddingEnd">@dimen/car_ui_toolbar_title_logo_padding</item>
+    </style>
+
+    <style name="Widget.CarUi.Toolbar.ProgressBar"
+           parent="@android:style/Widget.DeviceDefault.ProgressBar.Horizontal">
+    </style>
+
+    <style name="Widget.CarUi.Toolbar.NavIcon">
+        <item name="android:tint">@color/car_ui_toolbar_nav_icon_color</item>
+        <item name="android:src">@drawable/car_ui_icon_arrow_back</item>
+        <item name="android:background">@drawable/car_ui_toolbar_menu_item_icon_ripple</item>
+    </style>
+
+    <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">
+        <item name="android:drawableTint">@color/car_ui_toolbar_menu_item_icon_color</item>
+        <item name="android:drawablePadding">10dp</item>
+    </style>
+
+    <style name="Widget.CarUi.Toolbar.TextButton.WithIcon">
+        <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>
+        <item name="android:background">@android:color/transparent</item>
+    </style>
+
+    <!-- Style applied to the decoration view below the toolbar -->
+    <style name="Widget.CarUi.Toolbar.BottomView">
+        <item name="android:height">0.01dp</item>
+        <item name="android:background">@android:color/transparent</item>
+    </style>
+
+    <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"/>
+
+    <style name="Widget.CarUi.Toolbar.Search.SearchIcon" parent="Widget.CarUi.Toolbar"/>
+
+    <style name="Widget.CarUi.Toolbar.Search.CloseIcon" parent="Widget.CarUi.Toolbar">
+        <item name="android:background">@drawable/car_ui_toolbar_menu_item_icon_ripple</item>
+    </style>
+
+    <style name="Widget.CarUi.Toolbar.Tab"/>
+
+    <style name="Widget.CarUi.Toolbar.Tab.Container">
+        <item name="android:orientation">vertical</item>
+        <item name="android:paddingStart">@dimen/car_ui_toolbar_tab_padding_x</item>
+        <item name="android:paddingEnd">@dimen/car_ui_toolbar_tab_padding_x</item>
+        <item name="android:gravity">center</item>
+        <item name="android:background">?android:attr/selectableItemBackground</item>
+    </style>
+
+    <style name="Widget.CarUi.Toolbar.Tab.Icon">
+        <item name="android:scaleType">fitCenter</item>
+        <item name="android:tint">@color/car_ui_toolbar_tab_item_selector</item>
+        <item name="android:tintMode">src_in</item>
+    </style>
+
+    <style name="Widget.CarUi.Toolbar.Tab.Text">
+        <item name="android:singleLine">true</item>
+        <item name="android:gravity">center</item>
+        <item name="android:textAppearance">@style/TextAppearance.CarUi.Widget.Toolbar.Tab</item>
+    </style>
+
+    <style name="Widget.CarUi.CarUiRecyclerView">
+        <item name="android:scrollbars">vertical</item>
+    </style>
+
+    <style name="Widget.CarUi.AlertDialog"/>
+
+    <style name="Widget.CarUi.AlertDialog.HeaderContainer">
+        <item name="android:orientation">horizontal</item>
+        <item name="android:gravity">center_vertical|start</item>
+        <item name="android:paddingTop">18dp</item>
+        <item name="android:paddingBottom">18dp</item>
+    </style>
+
+    <style name="Widget.CarUi.AlertDialog.TitleContainer">
+        <item name="android:layout_marginStart">@dimen/car_ui_dialog_title_margin</item>
+        <item name="android:layout_marginEnd">@dimen/car_ui_dialog_title_margin</item>
+        <item name="android:orientation">vertical</item>
+    </style>
+
+    <style name="Widget.CarUi.AlertDialog.Icon">
+        <item name="android:layout_marginStart">@dimen/car_ui_dialog_title_margin</item>
+        <item name="android:scaleType">fitCenter</item>
+    </style>
+
+    <!-- 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>
+
+    <style name="Preference.CarUi.Category">
+        <item name="android:layout">@layout/car_ui_preference_category</item>
+        <!-- The title should not dim if the category is disabled, instead only the preference children should dim. -->
+        <item name="android:shouldDisableView">false</item>
+        <item name="android:selectable">false</item>
+    </style>
+
+    <style name="Preference.CarUi.CheckBoxPreference">
+        <item name="android:widgetLayout">@layout/car_ui_preference_widget_checkbox</item>
+    </style>
+
+    <style name="Preference.CarUi.DialogPreference">
+        <item name="android:positiveButtonText">@string/car_ui_dialog_preference_positive</item>
+        <item name="android:negativeButtonText">@string/car_ui_dialog_preference_negative</item>
+    </style>
+
+    <style name="Preference.CarUi.DialogPreference.EditTextPreference">
+        <item name="android:dialogLayout">@layout/car_ui_preference_dialog_edittext</item>
+    </style>
+
+    <style name="Preference.CarUi.DropDown">
+        <item name="android:layout">@layout/car_ui_preference_dropdown</item>
+    </style>
+
+    <style name="Preference.CarUi.Icon"/>
+
+    <style name="Preference.CarUi.Information">
+        <item name="android:enabled">false</item>
+        <item name="android:shouldDisableView">false</item>
+    </style>
+
+    <style name="Preference.CarUi.Preference"/>
+
+    <style name="Preference.CarUi.PreferenceScreen"/>
+
+    <style name="Preference.CarUi.SeekBarPreference">
+        <item name="android:layout">@layout/car_ui_preference_widget_seekbar</item>
+        <item name="adjustable">true</item>
+        <item name="showSeekBarValue">false</item>
+    </style>
+
+    <style name="Preference.CarUi.SwitchPreference">
+        <item name="android:widgetLayout">@layout/car_ui_preference_widget_switch</item>
+        <item name="android:switchTextOn">@string/car_ui_preference_switch_on</item>
+        <item name="android:switchTextOff">@string/car_ui_preference_switch_off</item>
+    </style>
+
+    <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>
+
+    <style name="PreferenceFragmentList.CarUi">
+        <item name="android:paddingTop">0dp</item>
+        <item name="android:paddingBottom">0dp</item>
+        <item name="android:paddingLeft">0dp</item>
+        <item name="android:paddingStart">0dp</item>
+        <item name="android:paddingRight">0dp</item>
+        <item name="android:paddingEnd">0dp</item>
+    </style>
+
+    <!-- TextAppearance -->
+
+    <style name="TextAppearance.CarUi" parent="android:TextAppearance.DeviceDefault">
+        <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>
+        <item name="android:textSize">@dimen/car_ui_preference_category_text_size</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.PreferenceSummary">
+        <item name="android:textColor">@color/car_ui_preference_summary_text_color</item>
+        <item name="android:textSize">@dimen/car_ui_preference_summary_text_size</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.PreferenceTitle">
+        <item name="android:textColor">@color/car_ui_preference_title_text_color</item>
+        <item name="android:textSize">@dimen/car_ui_preference_title_text_size</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.PreferenceEditTextDialogMessage">
+        <item name="android:textColor">@color/car_ui_preference_edit_text_dialog_message_text_color</item>
+        <item name="android:textSize">@dimen/car_ui_preference_edit_text_dialog_message_text_size</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.AlertDialog.Subtitle" parent="android:TextAppearance.DeviceDefault"/>
+
+    <style name="TextAppearance.CarUi.Widget" parent="android:TextAppearance.DeviceDefault.Widget"/>
+
+    <style name="TextAppearance.CarUi.Widget.Toolbar"/>
+
+    <style name="TextAppearance.CarUi.Widget.Toolbar.Title">
+        <item name="android:singleLine">true</item>
+        <item name="android:textSize">32sp</item>
+        <item name="android:letterSpacing">@dimen/car_ui_letter_spacing_body1</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.Widget.Toolbar.Tab">
+        <item name="android:textSize">24sp</item>
+        <item name="android:letterSpacing">@dimen/car_ui_letter_spacing_body3</item>
+        <item name="android:textColor">@color/car_ui_toolbar_tab_item_selector</item>
+        <item name="android:textStyle">normal</item>
+        <item name="android:textFontWeight">400</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.Widget.Toolbar.Tab.Selected">
+        <item name="android:textFontWeight">500</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.ListItem.Header">
+        <item name="android:fontFamily">@string/car_ui_list_item_header_font_family</item>
+        <item name="android:textColor">@color/car_ui_list_item_header_text_color</item>
+        <item name="android:textSize">@dimen/car_ui_list_item_header_text_size</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.ListItem">
+        <item name="android:textColor">@color/car_ui_list_item_title_text_color</item>
+        <item name="android:textSize">@dimen/car_ui_list_item_title_text_size</item>
+    </style>
+
+    <style name="TextAppearance.CarUi.ListItem.Body">
+        <item name="android:textSize">@dimen/car_ui_list_item_body_text_size</item>
+        <item name="android:textColor">@color/car_ui_list_item_body_text_color</item>
+    </style>
+
+</resources>
diff --git a/car-ui-lib/res/values/themes.xml b/car-ui-lib/res/values/themes.xml
new file mode 100644
index 0000000..34bbf3c
--- /dev/null
+++ b/car-ui-lib/res/values/themes.xml
@@ -0,0 +1,242 @@
+<?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">
+    <!-- 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>
+        <item name="windowActionBar">false</item>
+        <item name="windowActionBarOverlay">false</item>
+        <item name="windowActionModeOverlay">false</item>
+        <item name="actionBarPopupTheme">@null</item>
+
+        <item name="colorBackgroundFloating">?android:attr/colorBackgroundFloating</item>
+
+        <!-- Used by MediaRouter -->
+        <item name="isLightTheme">false</item>
+
+        <item name="selectableItemBackground">?android:attr/selectableItemBackground</item>
+        <item name="selectableItemBackgroundBorderless">?android:attr/selectableItemBackgroundBorderless</item>
+        <item name="borderlessButtonStyle">?android:attr/borderlessButtonStyle</item>
+        <item name="homeAsUpIndicator">?android:attr/homeAsUpIndicator</item>
+
+        <item name="dividerVertical">?android:attr/dividerVertical</item>
+        <item name="dividerHorizontal">?android:attr/dividerHorizontal</item>
+
+        <!-- Action Bar Styles -->
+        <item name="actionBarTabStyle">?android:attr/actionBarTabStyle</item>
+        <item name="actionBarTabBarStyle">?android:attr/actionBarTabBarStyle</item>
+        <item name="actionBarTabTextStyle">?android:attr/actionBarTabTextStyle</item>
+        <item name="actionButtonStyle">?android:attr/actionButtonStyle</item>
+        <item name="actionOverflowButtonStyle">?android:attr/actionOverflowButtonStyle</item>
+        <item name="actionOverflowMenuStyle">?android:attr/actionOverflowMenuStyle</item>
+        <item name="actionBarStyle">?android:attr/actionBarStyle</item>
+        <item name="actionBarSplitStyle">?android:attr/actionBarSplitStyle</item>
+        <item name="actionBarWidgetTheme">?android:attr/actionBarWidgetTheme</item>
+        <item name="actionBarTheme">?android:attr/actionBarTheme</item>
+        <item name="actionBarSize">?android:attr/actionBarSize</item>
+        <item name="actionBarDivider">?android:attr/actionBarDivider</item>
+        <item name="actionBarItemBackground">?android:attr/actionBarItemBackground</item>
+        <item name="actionMenuTextAppearance">?android:attr/actionMenuTextAppearance</item>
+        <item name="actionMenuTextColor">?android:attr/actionMenuTextColor</item>
+
+        <!-- Dropdown Spinner Attributes -->
+        <item name="actionDropDownStyle">?android:attr/actionDropDownStyle</item>
+
+        <!-- Action Mode -->
+        <item name="actionModeStyle">?android:attr/actionModeStyle</item>
+        <item name="actionModeBackground">?android:attr/actionModeBackground</item>
+        <item name="actionModeSplitBackground">?android:attr/actionModeSplitBackground</item>
+        <item name="actionModeCloseDrawable">?android:attr/actionModeCloseDrawable</item>
+        <item name="actionModeCloseButtonStyle">?android:attr/actionModeCloseButtonStyle</item>
+
+        <item name="actionModeCutDrawable">?android:attr/actionModeCutDrawable</item>
+        <item name="actionModeCopyDrawable">?android:attr/actionModeCopyDrawable</item>
+        <item name="actionModePasteDrawable">?android:attr/actionModePasteDrawable</item>
+        <item name="actionModeSelectAllDrawable">?android:attr/actionModeSelectAllDrawable</item>
+        <item name="actionModeShareDrawable">?android:attr/actionModeShareDrawable</item>
+
+        <!-- Panel attributes -->
+        <!-- TODO: panelMenuListWidth -->
+        <item name="panelMenuListWidth">@dimen/abc_panel_menu_list_width</item>
+        <!-- TODO: panelMenuListTheme -->
+        <item name="panelMenuListTheme">@style/Theme.AppCompat.CompactMenu</item>
+        <item name="panelBackground">?android:attr/panelBackground</item>
+        <item name="listChoiceBackgroundIndicator">?android:attr/listChoiceBackgroundIndicator</item>
+
+        <!-- List attributes -->
+        <item name="textAppearanceListItem">?android:attr/textAppearanceListItem</item>
+        <item name="textAppearanceListItemSmall">?android:attr/textAppearanceListItemSmall</item>
+        <item name="textAppearanceListItemSecondary">?android:attr/textAppearanceListItemSecondary</item>
+        <item name="listPreferredItemHeight">?android:attr/listPreferredItemHeight</item>
+        <item name="listPreferredItemHeightSmall">?android:attr/listPreferredItemHeightSmall</item>
+        <item name="listPreferredItemHeightLarge">?android:attr/listPreferredItemHeightLarge</item>
+        <item name="listPreferredItemPaddingLeft">?android:attr/listPreferredItemPaddingLeft</item>
+        <item name="listPreferredItemPaddingRight">?android:attr/listPreferredItemPaddingRight</item>
+
+        <!-- Spinner styles -->
+        <item name="spinnerStyle">?android:attr/spinnerStyle</item>
+
+        <!-- Required for use of support_simple_spinner_dropdown_item.xml -->
+        <item name="spinnerDropDownItemStyle">?android:attr/spinnerDropDownItemStyle</item>
+        <item name="dropdownListPreferredItemHeight">?attr/listPreferredItemHeightSmall</item>
+
+        <!-- Popup Menu styles -->
+        <item name="popupMenuStyle">?android:attr/popupMenuStyle</item>
+        <item name="textAppearanceLargePopupMenu">?android:attr/textAppearanceLargePopupMenu</item>
+        <item name="textAppearanceSmallPopupMenu">?android:attr/textAppearanceSmallPopupMenu</item>
+        <item name="textAppearancePopupMenuHeader">?android:attr/textAppearancePopupMenuHeader</item>
+        <item name="listPopupWindowStyle">?android:attr/listPopupWindowStyle</item>
+        <item name="dropDownListViewStyle">?android:attr/dropDownListViewStyle</item>
+        <item name="listMenuViewStyle">?android:attr/listMenuViewStyle</item>
+
+        <!-- SearchView attributes -->
+        <item name="searchViewStyle">?android:attr/searchViewStyle</item>
+        <!-- TODO: textColorSearchUrl -->
+        <item name="textColorSearchUrl">@color/abc_search_url_text</item>
+        <item name="textAppearanceSearchResultTitle">?android:attr/textAppearanceSearchResultTitle</item>
+        <item name="textAppearanceSearchResultSubtitle">?android:attr/textAppearanceSearchResultSubtitle</item>
+
+        <!-- ShareActionProvider attributes -->
+        <!-- TODO: activityChooserViewStyle -->
+        <item name="activityChooserViewStyle">@style/Widget.AppCompat.ActivityChooserView</item>
+
+        <!-- Toolbar styles -->
+        <item name="toolbarStyle">?android:attr/toolbarStyle</item>
+        <!-- TODO: toolbarNavigationButtonStyle -->
+        <item name="toolbarNavigationButtonStyle">@style/Widget.AppCompat.Toolbar.Button.Navigation</item>
+
+        <item name="editTextStyle">?android:attr/editTextStyle</item>
+        <item name="editTextBackground">?android:attr/editTextBackground</item>
+        <item name="editTextColor">?android:attr/editTextColor</item>
+        <item name="autoCompleteTextViewStyle">?android:attr/autoCompleteTextViewStyle</item>
+
+        <!-- Color palette -->
+        <item name="colorPrimaryDark">?android:attr/colorPrimaryDark</item>
+        <item name="colorPrimary">?android:attr/colorPrimary</item>
+        <item name="colorAccent">?android:attr/colorAccent</item>
+
+        <item name="colorControlNormal">?android:attr/colorControlNormal</item>
+        <item name="colorControlActivated">?android:attr/colorControlActivated</item>
+        <item name="colorControlHighlight">?android:attr/colorControlHighlight</item>
+        <item name="colorButtonNormal">?android:attr/colorButtonNormal</item>
+        <!-- TODO: colorSwitchThumbNormal -->
+        <item name="colorSwitchThumbNormal">@color/switch_thumb_material_dark</item>
+        <item name="controlBackground">?attr/selectableItemBackgroundBorderless</item>
+
+        <!-- TODO: drawerArrowStyle -->
+        <item name="drawerArrowStyle">@style/Widget.AppCompat.DrawerArrowToggle</item>
+
+        <item name="checkboxStyle">?android:attr/checkboxStyle</item>
+        <item name="radioButtonStyle">?android:attr/radioButtonStyle</item>
+        <item name="switchStyle">?android:attr/switchStyle</item>
+
+        <item name="ratingBarStyle">?android:attr/ratingBarStyle</item>
+        <item name="ratingBarStyleIndicator">?android:attr/ratingBarStyleIndicator</item>
+        <item name="ratingBarStyleSmall">?android:attr/ratingBarStyleSmall</item>
+        <item name="seekBarStyle">?android:attr/seekBarStyle</item>
+
+        <!-- Button styles -->
+        <item name="buttonStyle">?android:attr/buttonStyle</item>
+        <item name="buttonStyleSmall">?android:attr/buttonStyleSmall</item>
+
+        <item name="imageButtonStyle">?android:attr/imageButtonStyle</item>
+
+        <item name="buttonBarStyle">?android:attr/buttonBarStyle</item>
+        <item name="buttonBarButtonStyle">?android:attr/buttonBarButtonStyle</item>
+        <item name="buttonBarPositiveButtonStyle">?android:attr/buttonBarPositiveButtonStyle</item>
+        <item name="buttonBarNegativeButtonStyle">?android:attr/buttonBarNegativeButtonStyle</item>
+        <item name="buttonBarNeutralButtonStyle">?android:attr/buttonBarNeutralButtonStyle</item>
+
+        <!-- Dialog attributes -->
+        <item name="dialogTheme">?android:attr/dialogTheme</item>
+        <item name="dialogPreferredPadding">?android:attr/dialogPreferredPadding</item>
+        <item name="dialogCornerRadius">?android:attr/dialogCornerRadius</item>
+
+        <item name="alertDialogTheme">?android:attr/alertDialogTheme</item>
+        <item name="alertDialogStyle">?android:attr/alertDialogStyle</item>
+        <item name="alertDialogCenterButtons">false</item>
+        <item name="textColorAlertDialogListItem">?android:attr/textColorAlertDialogListItem</item>
+        <item name="listDividerAlertDialog">?android:attr/listDividerAlertDialog</item>
+
+        <!-- Define these here; ContextThemeWrappers around themes that define them should
+             always clear these values. -->
+        <item name="windowFixedWidthMajor">@null</item>
+        <item name="windowFixedWidthMinor">@null</item>
+        <item name="windowFixedHeightMajor">@null</item>
+        <item name="windowFixedHeightMinor">@null</item>
+
+        <!-- Tooltip attributes -->
+        <!-- TODO: tooltipFrameBackground -->
+        <item name="tooltipFrameBackground">@drawable/tooltip_frame_light</item>
+        <!-- TODO: tooltipForegroundColor -->
+        <item name="tooltipForegroundColor">@color/foreground_material_light</item>
+
+        <item name="colorError">?android:attr/colorError</item>
+
+        <!-- Attributes from: Platform.AppCompat -->
+
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowActionBar">false</item>
+
+        <item name="listChoiceIndicatorSingleAnimated">?android:attr/listChoiceIndicatorSingle</item>
+        <item name="listChoiceIndicatorMultipleAnimated">?android:attr/listChoiceIndicatorMultiple</item>
+
+        <item name="preferenceTheme">@style/CarUiPreferenceTheme</item>
+
+        <!-- Used by CarUiRecyclerView -->
+        <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>
+        <item name="dropdownPreferenceStyle">@style/Preference.CarUi.DropDown</item>
+        <item name="editTextPreferenceStyle">@style/Preference.CarUi.DialogPreference.EditTextPreference</item>
+        <item name="preferenceCategoryStyle">@style/Preference.CarUi.Category</item>
+        <item name="preferenceFragmentCompatStyle">@style/PreferenceFragment.CarUi</item>
+        <item name="preferenceFragmentListStyle">@style/PreferenceFragmentList.CarUi</item>
+        <item name="preferenceFragmentStyle">@style/PreferenceFragment.CarUi</item>
+        <item name="preferenceScreenStyle">@style/Preference.CarUi.PreferenceScreen</item>
+        <item name="preferenceStyle">@style/Preference.CarUi</item>
+        <item name="seekBarPreferenceStyle">@style/Preference.CarUi.SeekBarPreference</item>
+        <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
new file mode 100644
index 0000000..82a4d65
--- /dev/null
+++ b/car-ui-lib/res/values/values.xml
@@ -0,0 +1,24 @@
+<?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>
+    <!-- Toolbar -->
+
+    <!-- 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/settings.gradle b/car-ui-lib/settings.gradle
new file mode 100644
index 0000000..56f7f65
--- /dev/null
+++ b/car-ui-lib/settings.gradle
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+include ':PaintBooth'
+project(':PaintBooth').projectDir = new File('./tests/paintbooth')
+include ':RoboTests'
+project(':RoboTests').projectDir = new File('./tests/robotests')
+rootProject.name='Chassis'
diff --git a/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java b/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java
new file mode 100644
index 0000000..4803b45
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/AlertDialogBuilder.java
@@ -0,0 +1,677 @@
+/*
+ * 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;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.text.InputFilter;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+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
+ */
+public class AlertDialogBuilder {
+
+    private AlertDialog.Builder mBuilder;
+    private Context mContext;
+    private boolean mPositiveButtonSet;
+    private boolean mNeutralButtonSet;
+    private boolean mNegativeButtonSet;
+    private CharSequence mTitle;
+    private CharSequence mSubtitle;
+    private Drawable mIcon;
+
+    public AlertDialogBuilder(Context context) {
+        // Resource id specified as 0 uses the parent contexts resolved value for alertDialogTheme.
+        this(context, /* themeResId= */0);
+    }
+
+    public AlertDialogBuilder(Context context, int themeResId) {
+        mBuilder = new AlertDialog.Builder(context, themeResId);
+        mContext = context;
+    }
+
+    public Context getContext() {
+        return mBuilder.getContext();
+    }
+
+    /**
+     * Set the title using the given resource id.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setTitle(@StringRes int titleId) {
+        return setTitle(mContext.getText(titleId));
+    }
+
+    /**
+     * Set the title displayed in the {@link Dialog}.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setTitle(CharSequence title) {
+        mTitle = title;
+        mBuilder.setTitle(title);
+        return this;
+    }
+
+    /**
+     * Sets a subtitle to be displayed in the {@link Dialog}.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setSubtitle(@StringRes int subtitle) {
+        return setSubtitle(mContext.getString(subtitle));
+    }
+
+    /**
+     * Sets a subtitle to be displayed in the {@link Dialog}.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setSubtitle(CharSequence subtitle) {
+        mSubtitle = subtitle;
+        return this;
+    }
+
+    /**
+     * Set the message to display using the given resource id.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setMessage(@StringRes int messageId) {
+        mBuilder.setMessage(messageId);
+        return this;
+    }
+
+    /**
+     * Set the message to display.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setMessage(CharSequence message) {
+        mBuilder.setMessage(message);
+        return this;
+    }
+
+    /**
+     * Set the resource id of the {@link Drawable} to be used in the title.
+     * <p>
+     * Takes precedence over values set using {@link #setIcon(Drawable)}.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setIcon(@DrawableRes int iconId) {
+        return setIcon(mContext.getDrawable(iconId));
+    }
+
+    /**
+     * Set the {@link Drawable} to be used in the title.
+     * <p>
+     * <strong>Note:</strong> To ensure consistent styling, the drawable
+     * should be inflated or constructed using the alert dialog's themed
+     * context obtained via {@link #getContext()}.
+     *
+     * @return this Builder object to allow for chaining of calls to set
+     * methods
+     */
+    public AlertDialogBuilder setIcon(Drawable icon) {
+        mIcon = icon;
+        mBuilder.setIcon(icon);
+        return this;
+    }
+
+    /**
+     * Set an icon as supplied by a theme attribute. e.g.
+     * {@link android.R.attr#alertDialogIcon}.
+     * <p>
+     * Takes precedence over values set using {@link #setIcon(int)} or
+     * {@link #setIcon(Drawable)}.
+     *
+     * @param attrId ID of a theme attribute that points to a drawable resource.
+     */
+    public AlertDialogBuilder setIconAttribute(@AttrRes int attrId) {
+        mBuilder.setIconAttribute(attrId);
+        return this;
+    }
+
+    /**
+     * Set a listener to be invoked when the positive button of the dialog is pressed.
+     *
+     * @param textId The resource id of the text to display in the positive button
+     * @param listener The {@link DialogInterface.OnClickListener} to use.
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setPositiveButton(@StringRes int textId,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setPositiveButton(textId, listener);
+        mPositiveButtonSet = true;
+        return this;
+    }
+
+    /**
+     * Set a listener to be invoked when the positive button of the dialog is pressed.
+     *
+     * @param text The text to display in the positive button
+     * @param listener The {@link DialogInterface.OnClickListener} to use.
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setPositiveButton(CharSequence text,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setPositiveButton(text, listener);
+        mPositiveButtonSet = true;
+        return this;
+    }
+
+    /**
+     * Set a listener to be invoked when the negative button of the dialog is pressed.
+     *
+     * @param textId The resource id of the text to display in the negative button
+     * @param listener The {@link DialogInterface.OnClickListener} to use.
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setNegativeButton(@StringRes int textId,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setNegativeButton(textId, listener);
+        mNegativeButtonSet = true;
+        return this;
+    }
+
+    /**
+     * Set a listener to be invoked when the negative button of the dialog is pressed.
+     *
+     * @param text The text to display in the negative button
+     * @param listener The {@link DialogInterface.OnClickListener} to use.
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setNegativeButton(CharSequence text,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setNegativeButton(text, listener);
+        mNegativeButtonSet = true;
+        return this;
+    }
+
+    /**
+     * Set a listener to be invoked when the neutral button of the dialog is pressed.
+     *
+     * @param textId The resource id of the text to display in the neutral button
+     * @param listener The {@link DialogInterface.OnClickListener} to use.
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setNeutralButton(@StringRes int textId,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setNeutralButton(textId, listener);
+        mNeutralButtonSet = true;
+        return this;
+    }
+
+    /**
+     * Set a listener to be invoked when the neutral button of the dialog is pressed.
+     *
+     * @param text The text to display in the neutral button
+     * @param listener The {@link DialogInterface.OnClickListener} to use.
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setNeutralButton(CharSequence text,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setNeutralButton(text, listener);
+        mNeutralButtonSet = true;
+        return this;
+    }
+
+    /**
+     * Sets whether the dialog is cancelable or not.  Default is true.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setCancelable(boolean cancelable) {
+        mBuilder.setCancelable(cancelable);
+        return this;
+    }
+
+    /**
+     * Sets the callback that will be called if the dialog is canceled.
+     *
+     * <p>Even in a cancelable dialog, the dialog may be dismissed for reasons other than
+     * being canceled or one of the supplied choices being selected.
+     * If you are interested in listening for all cases where the dialog is dismissed
+     * and not just when it is canceled, see
+     * {@link #setOnDismissListener(android.content.DialogInterface.OnDismissListener)
+     * setOnDismissListener}.</p>
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     * @see #setCancelable(boolean)
+     * @see #setOnDismissListener(android.content.DialogInterface.OnDismissListener)
+     */
+    public AlertDialogBuilder setOnCancelListener(
+            DialogInterface.OnCancelListener onCancelListener) {
+        mBuilder.setOnCancelListener(onCancelListener);
+        return this;
+    }
+
+    /**
+     * Sets the callback that will be called when the dialog is dismissed for any reason.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setOnDismissListener(
+            DialogInterface.OnDismissListener onDismissListener) {
+        mBuilder.setOnDismissListener(onDismissListener);
+        return this;
+    }
+
+    /**
+     * Sets the callback that will be called if a key is dispatched to the dialog.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setOnKeyListener(DialogInterface.OnKeyListener onKeyListener) {
+        mBuilder.setOnKeyListener(onKeyListener);
+        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. This should be an array type i.e. R.array.foo
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setItems(@ArrayRes int itemsId,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setItems(itemsId, 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.
+     *
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setItems(CharSequence[] items,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setItems(items, listener);
+        return this;
+    }
+
+    /**
+     * This was not supposed to be in the Chassis API because it allows custom views.
+     *
+     * @deprecated Use {@link #setAdapter(CarUiListItemAdapter)} instead.
+     */
+    @Deprecated
+    public AlertDialogBuilder setAdapter(final ListAdapter adapter,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setAdapter(adapter, listener);
+        return this;
+    }
+
+    /**
+     * 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.
+     *
+     * @param cursor The {@link Cursor} to supply the list of items
+     * @param listener The listener that will be called when an item is clicked.
+     * @param labelColumn The column name on the cursor containing the string to display
+     * in the label.
+     * @return This Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setCursor(final Cursor cursor,
+            final DialogInterface.OnClickListener listener,
+            String labelColumn) {
+        mBuilder.setCursor(cursor, listener, labelColumn);
+        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.
+     * This should be an array type, e.g. R.array.foo. The list will have
+     * a check mark displayed to the right of the text for each checked
+     * item. Clicking on an item in the list will not dismiss the dialog.
+     * Clicking on a button will dismiss the dialog.
+     *
+     * @param itemsId the resource id of an array i.e. R.array.foo
+     * @param checkedItems specifies which items are checked. It should be null in which case no
+     * items are checked. If non null it must be exactly the same length as the array 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
+     */
+    public AlertDialogBuilder setMultiChoiceItems(@ArrayRes int itemsId, boolean[] checkedItems,
+            final DialogInterface.OnMultiChoiceClickListener listener) {
+        mBuilder.setMultiChoiceItems(itemsId, checkedItems, 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 each checked item. Clicking on an item in the list will not
+     * dismiss the dialog. Clicking on a button will dismiss the dialog.
+     *
+     * @param items the text of the items to be displayed in the list.
+     * @param checkedItems specifies which items are checked. It should be null in which case no
+     * items are checked. If non null it must be exactly the same length as the array 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
+     */
+    public AlertDialogBuilder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems,
+            final DialogInterface.OnMultiChoiceClickListener listener) {
+        mBuilder.setMultiChoiceItems(items, checkedItems, 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 each checked item. Clicking on an item in the list will not
+     * dismiss the dialog. Clicking on a button will dismiss the dialog.
+     *
+     * @param cursor the cursor used to provide the items.
+     * @param isCheckedColumn specifies the column name on the cursor to use to determine
+     * whether a checkbox is checked or not. It must return an integer value where 1
+     * means checked and 0 means unchecked.
+     * @param labelColumn The column name on the cursor containing the string to display in the
+     * label.
+     * @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
+     */
+    public AlertDialogBuilder setMultiChoiceItems(Cursor cursor, String isCheckedColumn,
+            String labelColumn,
+            final DialogInterface.OnMultiChoiceClickListener listener) {
+        mBuilder.setMultiChoiceItems(cursor, isCheckedColumn, labelColumn, 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. This should be an array type i.e.
+     * R.array.foo 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 itemsId the resource id of an array i.e. R.array.foo
+     * @param checkedItem specifies which item is checked. If -1 no items are checked.
+     * @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
+     */
+    public AlertDialogBuilder setSingleChoiceItems(@ArrayRes int itemsId, int checkedItem,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setSingleChoiceItems(itemsId, 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 cursor the cursor to retrieve the items from.
+     * @param checkedItem specifies which item is checked. If -1 no items are checked.
+     * @param labelColumn The column name on the cursor containing the string to display in the
+     * label.
+     * @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
+     */
+    public AlertDialogBuilder setSingleChoiceItems(Cursor cursor, int checkedItem,
+            String labelColumn,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setSingleChoiceItems(cursor, checkedItem, labelColumn, 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 items the items to be displayed.
+     * @param checkedItem specifies which item is checked. If -1 no items are checked.
+     * @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
+     */
+    public AlertDialogBuilder setSingleChoiceItems(CharSequence[] items, int checkedItem,
+            final DialogInterface.OnClickListener listener) {
+        mBuilder.setSingleChoiceItems(items, checkedItem, listener);
+        return this;
+    }
+
+    /**
+     * 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 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.
+     */
+    @Deprecated
+    public AlertDialogBuilder setSingleChoiceItems(CarUiRadioButtonListItemAdapter adapter,
+            final DialogInterface.OnClickListener 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;
+    }
+
+    /**
+     * Sets a listener to be invoked when an item in the list is selected.
+     *
+     * @param listener the listener to be invoked
+     * @return this Builder object to allow for chaining of calls to set methods
+     * @see AdapterView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener)
+     */
+    public AlertDialogBuilder setOnItemSelectedListener(
+            final AdapterView.OnItemSelectedListener listener) {
+        mBuilder.setOnItemSelectedListener(listener);
+        return this;
+    }
+
+    /**
+     * Sets a custom edit text box within the alert dialog.
+     *
+     * @param prompt the string that will be set on the edit text view
+     * @param textChangedListener textWatcher whose methods are called whenever this TextView's text
+     * changes {@link null} otherwise.
+     * @param inputFilters list of input filters, {@link null} if no filter is needed
+     * @param inputType See {@link EditText#setInputType(int)}, except
+     *                  {@link android.text.InputType#TYPE_NULL} will not be set.
+     * @return this Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setEditBox(String prompt, TextWatcher textChangedListener,
+            InputFilter[] inputFilters, int inputType) {
+        View contentView = LayoutInflater.from(mContext).inflate(
+                R.layout.car_ui_alert_dialog_edit_text, null);
+
+        EditText editText = contentView.requireViewById(R.id.textbox);
+        editText.setText(prompt);
+
+        if (textChangedListener != null) {
+            editText.addTextChangedListener(textChangedListener);
+        }
+
+        if (inputFilters != null) {
+            editText.setFilters(inputFilters);
+        }
+
+        if (inputType != 0) {
+            editText.setInputType(inputType);
+        }
+
+        mBuilder.setView(contentView);
+        return this;
+    }
+
+    /**
+     * Sets a custom edit text box within the alert dialog.
+     *
+     * @param prompt the string that will be set on the edit text view
+     * @param textChangedListener textWatcher whose methods are called whenever this TextView's text
+     * changes {@link null} otherwise.
+     * @param inputFilters list of input filters, {@link null} if no filter is needed
+     * @return this Builder object to allow for chaining of calls to set methods
+     */
+    public AlertDialogBuilder setEditBox(String prompt, TextWatcher textChangedListener,
+            InputFilter[] inputFilters) {
+        return setEditBox(prompt, textChangedListener, inputFilters, 0);
+    }
+
+
+    /** Final steps common to both {@link #create()} and {@link #show()} */
+    private void prepareDialog() {
+        if (mSubtitle != null) {
+
+            View customTitle = LayoutInflater.from(mContext).inflate(
+                    R.layout.car_ui_alert_dialog_title_with_subtitle, null);
+
+            TextView mTitleView = customTitle.requireViewById(R.id.alertTitle);
+            TextView mSubtitleView = customTitle.requireViewById(R.id.alertSubtitle);
+            ImageView mIconView = customTitle.requireViewById(R.id.icon);
+
+            mTitleView.setText(mTitle);
+            mSubtitleView.setText(mSubtitle);
+            mIconView.setImageDrawable(mIcon);
+            mIconView.setVisibility(mIcon != null ? View.VISIBLE : View.GONE);
+            mBuilder.setCustomTitle(customTitle);
+        }
+
+        if (!mNeutralButtonSet && !mNegativeButtonSet && !mPositiveButtonSet) {
+            String mDefaultButtonText = mContext.getString(
+                    R.string.car_ui_alert_dialog_default_button);
+            mBuilder.setNegativeButton(mDefaultButtonText, (dialog, which) -> {
+            });
+        }
+    }
+
+    /**
+     * Creates an {@link AlertDialog} with the arguments supplied to this
+     * builder.
+     * <p>
+     * Calling this method does not display the dialog. If no additional
+     * processing is needed, {@link #show()} may be called instead to both
+     * create and display the dialog.
+     */
+    public AlertDialog create() {
+        prepareDialog();
+        return mBuilder.create();
+    }
+
+    /**
+     * Creates an {@link AlertDialog} with the arguments supplied to this
+     * builder and immediately displays the dialog.
+     * <p>
+     * Calling this method is functionally identical to:
+     * <pre>
+     *     AlertDialog dialog = builder.create();
+     *     dialog.show();
+     * </pre>
+     */
+    public AlertDialog show() {
+        prepareDialog();
+        return mBuilder.show();
+    }
+}
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/CarUiDialogFragment.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiDialogFragment.java
new file mode 100644
index 0000000..6a848b8
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiDialogFragment.java
@@ -0,0 +1,234 @@
+/*
+ * 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.preference;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import androidx.preference.DialogPreference;
+
+/**
+ * Abstract base class which presents a dialog associated with a {@link
+ * androidx.preference.DialogPreference}. Since the preference object may not be available during
+ * fragment re-creation, the necessary information for displaying the dialog is read once during
+ * the initial call to {@link #onCreate(Bundle)} and saved/restored in the saved instance state.
+ * Custom subclasses should also follow this pattern.
+ *
+ * <p>Note: this is borrowed as-is from {@link androidx.preference.PreferenceDialogFragmentCompat}
+ * with updates to formatting to match the project style and the removal of the {@link
+ * DialogPreference.TargetFragment} interface requirement. See {@link PreferenceDialogFragment}
+ * for a version of this class with the check preserved. Automotive applications should use
+ * children of this fragment in order to launch the system themed platform {@link AlertDialog}
+ * instead of the one in the support library.
+ */
+
+public abstract class CarUiDialogFragment extends DialogFragment implements
+        DialogInterface.OnClickListener {
+
+    private static final String SAVE_STATE_TITLE = "CarUiDialogFragment.title";
+    private static final String SAVE_STATE_POSITIVE_TEXT = "CarUiDialogFragment.positiveText";
+    private static final String SAVE_STATE_NEGATIVE_TEXT = "CarUiDialogFragment.negativeText";
+    private static final String SAVE_STATE_MESSAGE = "CarUiDialogFragment.message";
+    private static final String SAVE_STATE_LAYOUT = "CarUiDialogFragment.layout";
+    private static final String SAVE_STATE_ICON = "CarUiDialogFragment.icon";
+
+    protected CharSequence mDialogTitle;
+    protected CharSequence mPositiveButtonText;
+    protected CharSequence mNegativeButtonText;
+    protected CharSequence mDialogMessage;
+    @LayoutRes
+    protected int mDialogLayoutRes;
+
+    protected BitmapDrawable mDialogIcon;
+
+    /** Which button was clicked. */
+    private int mWhichButtonClicked;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (savedInstanceState != null) {
+            mDialogTitle = savedInstanceState.getCharSequence(SAVE_STATE_TITLE);
+            mPositiveButtonText = savedInstanceState.getCharSequence(SAVE_STATE_POSITIVE_TEXT);
+            mNegativeButtonText = savedInstanceState.getCharSequence(SAVE_STATE_NEGATIVE_TEXT);
+            mDialogMessage = savedInstanceState.getCharSequence(SAVE_STATE_MESSAGE);
+            mDialogLayoutRes = savedInstanceState.getInt(SAVE_STATE_LAYOUT, 0);
+            Bitmap bitmap = savedInstanceState.getParcelable(SAVE_STATE_ICON);
+            if (bitmap != null) {
+                mDialogIcon = new BitmapDrawable(getResources(), bitmap);
+            }
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putCharSequence(SAVE_STATE_TITLE, mDialogTitle);
+        outState.putCharSequence(SAVE_STATE_POSITIVE_TEXT, mPositiveButtonText);
+        outState.putCharSequence(SAVE_STATE_NEGATIVE_TEXT, mNegativeButtonText);
+        outState.putCharSequence(SAVE_STATE_MESSAGE, mDialogMessage);
+        outState.putInt(SAVE_STATE_LAYOUT, mDialogLayoutRes);
+        if (mDialogIcon != null) {
+            outState.putParcelable(SAVE_STATE_ICON, mDialogIcon.getBitmap());
+        }
+    }
+
+    @Override
+    @NonNull
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        Context context = getActivity();
+        mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(context)
+                .setTitle(mDialogTitle)
+                .setIcon(mDialogIcon)
+                .setPositiveButton(mPositiveButtonText, this)
+                .setNegativeButton(mNegativeButtonText, this);
+
+        View contentView = onCreateDialogView(context);
+        if (contentView != null) {
+            onBindDialogView(contentView);
+            builder.setView(contentView);
+        } else {
+            builder.setMessage(mDialogMessage);
+        }
+
+        onPrepareDialogBuilder(builder);
+
+        // Create the dialog
+        Dialog dialog = builder.create();
+        if (needInputMethod()) {
+            // Request input only after the dialog is shown. This is to prevent an issue where the
+            // dialog view collapsed the content on small displays.
+            dialog.setOnShowListener(d -> requestInputMethod(dialog));
+        }
+
+        return dialog;
+    }
+
+    /**
+     * Prepares the dialog builder to be shown when the preference is clicked. Use this to set
+     * custom properties on the dialog.
+     *
+     * <p>Do not {@link AlertDialog.Builder#create()} or {@link AlertDialog.Builder#show()}.
+     */
+    protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+    }
+
+    /**
+     * Returns whether the preference needs to display a soft input method when the dialog is
+     * displayed. Default is false. Subclasses should override this method if they need the soft
+     * input method brought up automatically.
+     *
+     * <p>Note: Ensure your subclass manually requests focus (ideally in {@link
+     * #onBindDialogView(View)}) for the input field in order to
+     * correctly attach the input method to the field.
+     */
+    protected boolean needInputMethod() {
+        return false;
+    }
+
+    /**
+     * Sets the required flags on the dialog window to enable input method window to show up.
+     */
+    private void requestInputMethod(Dialog dialog) {
+        Window window = dialog.getWindow();
+        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+    }
+
+    /**
+     * Creates the content view for the dialog (if a custom content view is required). By default,
+     * it inflates the dialog layout resource if it is set.
+     *
+     * @return the content View for the dialog.
+     * @see DialogPreference#setLayoutResource(int)
+     */
+    protected View onCreateDialogView(Context context) {
+        int resId = mDialogLayoutRes;
+        if (resId == 0) {
+            return null;
+        }
+
+        LayoutInflater inflater = LayoutInflater.from(context);
+        return inflater.inflate(resId, null);
+    }
+
+    /**
+     * Binds views in the content View of the dialog to data.
+     *
+     * <p>Make sure to call through to the superclass implementation.
+     *
+     * @param view the content View of the dialog, if it is custom.
+     */
+    @CallSuper
+    protected void onBindDialogView(View view) {
+        View dialogMessageView = view.findViewById(android.R.id.message);
+
+        if (dialogMessageView != null) {
+            CharSequence message = mDialogMessage;
+            int newVisibility = View.GONE;
+
+            if (!TextUtils.isEmpty(message)) {
+                if (dialogMessageView instanceof TextView) {
+                    ((TextView) dialogMessageView).setText(message);
+                }
+
+                newVisibility = View.VISIBLE;
+            }
+
+            if (dialogMessageView.getVisibility() != newVisibility) {
+                dialogMessageView.setVisibility(newVisibility);
+            }
+        }
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        mWhichButtonClicked = which;
+    }
+
+    @Override
+    public void onDismiss(DialogInterface dialog) {
+        super.onDismiss(dialog);
+        onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
+    }
+
+    /**
+     * Called when the dialog is dismissed.
+     *
+     * @param positiveResult {@code true} if the dialog was dismissed with {@link
+     *                       DialogInterface#BUTTON_POSITIVE}.
+     */
+    protected abstract void onDialogClosed(boolean positiveResult);
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/CarUiDropDownPreference.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiDropDownPreference.java
new file mode 100644
index 0000000..56134b3
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiDropDownPreference.java
@@ -0,0 +1,77 @@
+/*
+ * 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.preference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.preference.DropDownPreference;
+
+import com.android.car.ui.R;
+
+/**
+ * This class extends the base {@link DropDownPreference} class. Adds the drawable icon to
+ * the preference.
+ */
+public class CarUiDropDownPreference extends DropDownPreference {
+
+    private final Context mContext;
+
+    public CarUiDropDownPreference(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    public CarUiDropDownPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+    }
+
+    public CarUiDropDownPreference(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mContext = context;
+    }
+
+    public CarUiDropDownPreference(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mContext = context;
+    }
+
+    /**
+     * Instead of displaying a drop-down that is not car optimized, have drop-down preferences
+     * mirror the behavior of list preferences.
+     */
+    @Override
+    protected void onClick() {
+        getPreferenceManager().showDialog(this);
+    }
+
+    @Override
+    public void onAttached() {
+        super.onAttached();
+
+        boolean showChevron = mContext.getResources().getBoolean(
+                R.bool.car_ui_preference_show_chevron);
+
+        if (!showChevron) {
+            return;
+        }
+
+        setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/CarUiEditTextPreference.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiEditTextPreference.java
new file mode 100644
index 0000000..4480714
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiEditTextPreference.java
@@ -0,0 +1,73 @@
+/*
+ * 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.preference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.preference.EditTextPreference;
+
+import com.android.car.ui.R;
+
+/**
+ * This class extends the base {@link EditTextPreference} class. Adds the drawable icon to
+ * the preference.
+ */
+public class CarUiEditTextPreference extends EditTextPreference {
+
+    private final Context mContext;
+    private boolean mShowChevron = true;
+
+    public CarUiEditTextPreference(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mContext = context;
+    }
+
+    public CarUiEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mContext = context;
+    }
+
+    public CarUiEditTextPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+    }
+
+    public CarUiEditTextPreference(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    @Override
+    public void onAttached() {
+        super.onAttached();
+
+        boolean allowChevron = mContext.getResources().getBoolean(
+                R.bool.car_ui_preference_show_chevron);
+
+        if (!allowChevron || !mShowChevron) {
+            return;
+        }
+
+        setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
+    }
+
+    public void setShowChevron(boolean showChevron) {
+        mShowChevron = showChevron;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/CarUiListPreference.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiListPreference.java
new file mode 100644
index 0000000..24d940c
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiListPreference.java
@@ -0,0 +1,68 @@
+/*
+ * 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.preference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.preference.ListPreference;
+
+import com.android.car.ui.R;
+
+/**
+ * This class extends the base {@link ListPreference} class. Adds the drawable icon to
+ * the preference.
+ */
+public class CarUiListPreference extends ListPreference {
+
+    private final Context mContext;
+
+    public CarUiListPreference(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mContext = context;
+    }
+
+    public CarUiListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mContext = context;
+    }
+
+    public CarUiListPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+    }
+
+    public CarUiListPreference(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    @Override
+    public void onAttached() {
+        super.onAttached();
+
+        boolean showChevron = mContext.getResources().getBoolean(
+                R.bool.car_ui_preference_show_chevron);
+
+        if (!showChevron) {
+            return;
+        }
+
+        setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/CarUiMultiSelectListPreference.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiMultiSelectListPreference.java
new file mode 100644
index 0000000..4752082
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiMultiSelectListPreference.java
@@ -0,0 +1,72 @@
+/*
+ * 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.preference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.preference.MultiSelectListPreference;
+
+import com.android.car.ui.R;
+
+/**
+ * This class extends the base {@link CarUiMultiSelectListPreference} class. Adds the drawable icon
+ * to the preference.
+ */
+public class CarUiMultiSelectListPreference extends MultiSelectListPreference {
+
+    private final Context mContext;
+
+    public CarUiMultiSelectListPreference(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    public CarUiMultiSelectListPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+    }
+
+    public CarUiMultiSelectListPreference(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mContext = context;
+    }
+
+    public CarUiMultiSelectListPreference(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mContext = context;
+    }
+
+    protected boolean[] getSelectedItems() {
+        return super.getSelectedItems();
+    }
+
+    @Override
+    public void onAttached() {
+        super.onAttached();
+
+        boolean showChevron = mContext.getResources().getBoolean(
+                R.bool.car_ui_preference_show_chevron);
+
+        if (!showChevron) {
+            return;
+        }
+
+        setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/CarUiPreference.java b/car-ui-lib/src/com/android/car/ui/preference/CarUiPreference.java
new file mode 100644
index 0000000..5db22cf
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/CarUiPreference.java
@@ -0,0 +1,86 @@
+/*
+ * 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.preference;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import androidx.preference.Preference;
+
+import com.android.car.ui.R;
+
+/**
+ * This class extends the base {@link Preference} class. Adds the support to add a drawable icon to
+ * the preference if there is one of fragment, intent or onPreferenceClickListener set.
+ */
+public class CarUiPreference extends Preference {
+
+    private Context mContext;
+    private boolean mShowChevron;
+
+    public CarUiPreference(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public CarUiPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, R.style.Preference_CarUi_Preference);
+    }
+
+    public CarUiPreference(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.carUiPreferenceStyle);
+    }
+
+    public CarUiPreference(Context context) {
+        this(context, null);
+    }
+
+    public void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        mContext = context;
+
+        TypedArray a = getContext().obtainStyledAttributes(
+                attrs,
+                R.styleable.CarUiPreference,
+                defStyleAttr,
+                defStyleRes);
+
+        mShowChevron = a.getBoolean(R.styleable.CarUiPreference_showChevron, true);
+    }
+
+    @Override
+    public void onAttached() {
+        super.onAttached();
+
+        boolean allowChevron = mContext.getResources().getBoolean(
+                R.bool.car_ui_preference_show_chevron);
+
+        if (!allowChevron || !mShowChevron) {
+            return;
+        }
+
+        if (getOnPreferenceClickListener() != null || getIntent() != null
+                || getFragment() != null) {
+            setWidgetLayoutResource(R.layout.car_ui_preference_chevron);
+        }
+    }
+
+    public void setShowChevron(boolean showChevron) {
+        mShowChevron = showChevron;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/EditTextPreferenceDialogFragment.java b/car-ui-lib/src/com/android/car/ui/preference/EditTextPreferenceDialogFragment.java
new file mode 100644
index 0000000..97a58d2
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/EditTextPreferenceDialogFragment.java
@@ -0,0 +1,139 @@
+/*
+ * 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.preference;
+
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.text.InputType;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.preference.EditTextPreference;
+
+/**
+ * Presents a dialog with an {@link EditText} associated with an {@link EditTextPreference}.
+ *
+ * <p>Note: this is borrowed as-is from androidx.preference.EditTextPreferenceDialogFragmentCompat
+ * with updates to formatting to match the project style. Automotive applications should use this
+ * implementations in order to launch the system themed platform {@link AlertDialog} instead of the
+ * one in the support library.
+ */
+public class EditTextPreferenceDialogFragment extends PreferenceDialogFragment implements
+        TextView.OnEditorActionListener {
+
+    private static final String SAVE_STATE_TEXT = "EditTextPreferenceDialogFragment.text";
+
+    private EditText mEditText;
+    private CharSequence mText;
+    private boolean mAllowEnterToSubmit = true;
+
+    /**
+     * Returns a new instance of {@link EditTextPreferenceDialogFragment} for the {@link
+     * EditTextPreference} with the given {@code key}.
+     */
+    public static EditTextPreferenceDialogFragment newInstance(String key) {
+        EditTextPreferenceDialogFragment fragment =
+                new EditTextPreferenceDialogFragment();
+        Bundle b = new Bundle(/* capacity= */ 1);
+        b.putString(ARG_KEY, key);
+        fragment.setArguments(b);
+        return fragment;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState == null) {
+            mText = getEditTextPreference().getText();
+        } else {
+            mText = savedInstanceState.getCharSequence(SAVE_STATE_TEXT);
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putCharSequence(SAVE_STATE_TEXT, mText);
+    }
+
+    @Override
+    protected void onBindDialogView(View view) {
+        super.onBindDialogView(view);
+
+        mEditText = view.findViewById(android.R.id.edit);
+
+        if (mEditText == null) {
+            throw new IllegalStateException(
+                    "Dialog view must contain an EditText with id @android:id/edit");
+        }
+
+        mEditText.requestFocus();
+        mEditText.setText(mText);
+        mEditText.setInputType(InputType.TYPE_CLASS_TEXT);
+        mEditText.setImeOptions(EditorInfo.IME_ACTION_DONE);
+        mEditText.setOnEditorActionListener(this);
+
+        // Place cursor at the end
+        mEditText.setSelection(mEditText.getText().length());
+    }
+
+    private EditTextPreference getEditTextPreference() {
+        return (EditTextPreference) getPreference();
+    }
+
+    @Override
+    protected boolean needInputMethod() {
+        return true;
+    }
+
+    @Override
+    protected void onDialogClosed(boolean positiveResult) {
+        if (positiveResult) {
+            String value = mEditText.getText().toString();
+            if (getEditTextPreference().callChangeListener(value)) {
+                getEditTextPreference().setText(value);
+            }
+        }
+    }
+
+    /** Allows enabling and disabling the ability to press enter to dismiss the dialog. */
+    public void setAllowEnterToSubmit(boolean isAllowed) {
+        mAllowEnterToSubmit = isAllowed;
+    }
+
+    /** Allows verifying if enter to submit is currently enabled. */
+    public boolean getAllowEnterToSubmit() {
+        return mAllowEnterToSubmit;
+    }
+
+    @Override
+    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (actionId == EditorInfo.IME_ACTION_DONE && mAllowEnterToSubmit) {
+            CharSequence newValue = v.getText();
+
+            getEditTextPreference().callChangeListener(newValue);
+            dismiss();
+
+            return true;
+        }
+        return false;
+    }
+}
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
new file mode 100644
index 0000000..2543fb5
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/ListPreferenceFragment.java
@@ -0,0 +1,178 @@
+/*
+ * 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.preference;
+
+import static com.android.car.ui.preference.PreferenceDialogFragment.ARG_KEY;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.preference.DialogPreference;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+
+import com.android.car.ui.R;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiListItem;
+import com.android.car.ui.recyclerview.CarUiListItemAdapter;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.toolbar.Toolbar;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A fragment that provides a layout with a list of options associated with a {@link
+ * ListPreference}.
+ */
+public class ListPreferenceFragment extends Fragment {
+
+    private ListPreference mPreference;
+    private CarUiContentListItem mSelectedItem;
+
+    /**
+     * Returns a new instance of {@link ListPreferenceFragment} for the {@link ListPreference} with
+     * the given {@code key}.
+     */
+    static ListPreferenceFragment newInstance(String key) {
+        ListPreferenceFragment fragment = new ListPreferenceFragment();
+        Bundle b = new Bundle(/* capacity= */ 1);
+        b.putString(ARG_KEY, key);
+        fragment.setArguments(b);
+        return fragment;
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(
+            @NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.car_ui_list_preference, container, false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        final CarUiRecyclerView carUiRecyclerView = view.requireViewById(R.id.list);
+        final Toolbar toolbar = view.requireViewById(R.id.toolbar);
+
+        carUiRecyclerView.setPadding(0, toolbar.getHeight(), 0, 0);
+        toolbar.registerToolbarHeightChangeListener(newHeight -> {
+            if (carUiRecyclerView.getPaddingTop() == newHeight) {
+                return;
+            }
+
+            int oldHeight = carUiRecyclerView.getPaddingTop();
+            carUiRecyclerView.setPadding(0, newHeight, 0, 0);
+            carUiRecyclerView.scrollBy(0, oldHeight - newHeight);
+        });
+
+        carUiRecyclerView.setClipToPadding(false);
+        mPreference = getListPreference();
+        toolbar.setTitle(mPreference.getTitle());
+
+        CharSequence[] entries = mPreference.getEntries();
+        CharSequence[] entryValues = mPreference.getEntryValues();
+
+        if (entries == null || entryValues == null) {
+            throw new IllegalStateException(
+                    "ListPreference requires an entries array and an entryValues array.");
+        }
+
+        if (entries.length != entryValues.length) {
+            throw new IllegalStateException(
+                    "ListPreference entries array length does not match entryValues array length.");
+        }
+
+        int selectedEntryIndex = mPreference.findIndexOfValue(mPreference.getValue());
+        List<CarUiListItem> listItems = new ArrayList<>();
+        CarUiListItemAdapter adapter = new CarUiListItemAdapter(listItems);
+
+        for (int i = 0; i < entries.length; i++) {
+            String entry = entries[i].toString();
+            CarUiContentListItem item = new CarUiContentListItem(
+                    CarUiContentListItem.Action.RADIO_BUTTON);
+            item.setTitle(entry);
+
+            if (i == selectedEntryIndex) {
+                item.setChecked(true);
+                mSelectedItem = item;
+            }
+
+            item.setOnCheckedChangeListener((listItem, isChecked) -> {
+                if (mSelectedItem != null) {
+                    mSelectedItem.setChecked(false);
+                    adapter.notifyItemChanged(listItems.indexOf(mSelectedItem));
+                }
+                mSelectedItem = listItem;
+            });
+
+            listItems.add(item);
+        }
+
+        toolbar.registerOnBackListener(() -> {
+            if (mSelectedItem != null) {
+                int selectedIndex = listItems.indexOf(mSelectedItem);
+                String entryValue = entryValues[selectedIndex].toString();
+
+                if (mPreference.callChangeListener(entryValue)) {
+                    mPreference.setValue(entryValue);
+                }
+            }
+
+            return false;
+        });
+
+        carUiRecyclerView.setAdapter(adapter);
+    }
+
+    private ListPreference getListPreference() {
+        if (getArguments() == null) {
+            throw new IllegalStateException("Preference arguments cannot be null");
+        }
+
+        String key = getArguments().getString(ARG_KEY);
+        DialogPreference.TargetFragment fragment =
+                (DialogPreference.TargetFragment) getTargetFragment();
+
+        if (key == null) {
+            throw new IllegalStateException(
+                    "ListPreference key not found in Fragment arguments");
+        }
+
+        if (fragment == null) {
+            throw new IllegalStateException(
+                    "Target fragment must be registered before displaying ListPreference "
+                            + "screen.");
+        }
+
+        Preference preference = fragment.findPreference(key);
+
+        if (!(preference instanceof ListPreference)) {
+            throw new IllegalStateException(
+                    "Cannot use ListPreferenceFragment with a preference that is not of type "
+                            + "ListPreference");
+        }
+
+        return (ListPreference) preference;
+    }
+}
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
new file mode 100644
index 0000000..c445142
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/MultiSelectListPreferenceFragment.java
@@ -0,0 +1,174 @@
+/*
+ * 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.preference;
+
+import static com.android.car.ui.preference.PreferenceDialogFragment.ARG_KEY;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.preference.DialogPreference;
+import androidx.preference.Preference;
+
+import com.android.car.ui.R;
+import com.android.car.ui.recyclerview.CarUiContentListItem;
+import com.android.car.ui.recyclerview.CarUiListItem;
+import com.android.car.ui.recyclerview.CarUiListItemAdapter;
+import com.android.car.ui.recyclerview.CarUiRecyclerView;
+import com.android.car.ui.toolbar.Toolbar;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A fragment that provides a layout with a list of options associated with a {@link
+ * CarUiMultiSelectListPreference}.
+ */
+public class MultiSelectListPreferenceFragment extends Fragment {
+
+    private CarUiMultiSelectListPreference mPreference;
+    private Set<String> mNewValues;
+
+    /**
+     * Returns a new instance of {@link MultiSelectListPreferenceFragment} for the {@link
+     * CarUiMultiSelectListPreference} with the given {@code key}.
+     */
+    static MultiSelectListPreferenceFragment newInstance(String key) {
+        MultiSelectListPreferenceFragment fragment = new MultiSelectListPreferenceFragment();
+        Bundle b = new Bundle(/* capacity= */ 1);
+        b.putString(ARG_KEY, key);
+        fragment.setArguments(b);
+        return fragment;
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(
+            @NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.car_ui_list_preference, container, false);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        final CarUiRecyclerView recyclerView = view.requireViewById(R.id.list);
+        final Toolbar toolbar = view.requireViewById(R.id.toolbar);
+
+        recyclerView.setPadding(0, toolbar.getHeight(), 0, 0);
+        toolbar.registerToolbarHeightChangeListener(newHeight -> {
+            if (recyclerView.getPaddingTop() == newHeight) {
+                return;
+            }
+
+            int oldHeight = recyclerView.getPaddingTop();
+            recyclerView.setPadding(0, newHeight, 0, 0);
+            recyclerView.scrollBy(0, oldHeight - newHeight);
+        });
+
+        mPreference = getPreference();
+
+        recyclerView.setClipToPadding(false);
+        toolbar.setTitle(mPreference.getTitle());
+
+        mNewValues = new HashSet<>(mPreference.getValues());
+        CharSequence[] entries = mPreference.getEntries();
+        CharSequence[] entryValues = mPreference.getEntryValues();
+
+        if (entries == null || entryValues == null) {
+            throw new IllegalStateException(
+                    "MultiSelectListPreference requires an entries array and an entryValues array"
+                            + ".");
+        }
+
+        if (entries.length != entryValues.length) {
+            throw new IllegalStateException(
+                    "MultiSelectListPreference entries array length does not match entryValues "
+                            + "array length.");
+        }
+
+        List<CarUiListItem> listItems = new ArrayList<>();
+        boolean[] selectedItems = mPreference.getSelectedItems();
+
+        for (int i = 0; i < entries.length; i++) {
+            String entry = entries[i].toString();
+            String entryValue = entryValues[i].toString();
+            CarUiContentListItem item = new CarUiContentListItem(
+                    CarUiContentListItem.Action.CHECK_BOX);
+            item.setTitle(entry);
+            item.setChecked(selectedItems[i]);
+            item.setOnCheckedChangeListener((listItem, isChecked) -> {
+                if (isChecked) {
+                    mNewValues.add(entryValue);
+                } else {
+                    mNewValues.remove(entryValue);
+                }
+            });
+
+            listItems.add(item);
+        }
+
+        CarUiListItemAdapter adapter = new CarUiListItemAdapter(listItems);
+        recyclerView.setAdapter(adapter);
+
+        toolbar.registerOnBackListener(() -> {
+            if (mPreference.callChangeListener(mNewValues)) {
+                mPreference.setValues(mNewValues);
+            }
+
+            return false;
+        });
+    }
+
+    private CarUiMultiSelectListPreference getPreference() {
+        if (getArguments() == null) {
+            throw new IllegalStateException("Preference arguments cannot be null");
+        }
+
+        String key = getArguments().getString(ARG_KEY);
+        DialogPreference.TargetFragment fragment =
+                (DialogPreference.TargetFragment) getTargetFragment();
+
+        if (key == null) {
+            throw new IllegalStateException(
+                    "MultiSelectListPreference key not found in Fragment arguments");
+        }
+
+        if (fragment == null) {
+            throw new IllegalStateException(
+                    "Target fragment must be registered before displaying "
+                            + "MultiSelectListPreference screen.");
+        }
+
+        Preference preference = fragment.findPreference(key);
+
+        if (!(preference instanceof CarUiMultiSelectListPreference)) {
+            throw new IllegalStateException(
+                    "Cannot use MultiSelectListPreferenceFragment with a preference that is "
+                            + "not of type CarUiMultiSelectListPreference");
+        }
+
+        return (CarUiMultiSelectListPreference) preference;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/preference/PreferenceDialogFragment.java b/car-ui-lib/src/com/android/car/ui/preference/PreferenceDialogFragment.java
new file mode 100644
index 0000000..8b2e79e
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/PreferenceDialogFragment.java
@@ -0,0 +1,103 @@
+/*
+ * 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.preference;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+
+import androidx.fragment.app.Fragment;
+import androidx.preference.DialogPreference;
+import androidx.preference.PreferenceFragmentCompat;
+
+/**
+ * Abstract base class which presents a dialog associated with a {@link
+ * androidx.preference.DialogPreference}. Since the preference object may not be available during
+ * fragment re-creation, the necessary information for displaying the dialog is read once during
+ * the initial call to {@link #onCreate(Bundle)} and saved/restored in the saved instance state.
+ * Custom subclasses should also follow this pattern.
+ *
+ * <p>Note: this has the same functionality and interface as {@link
+ * androidx.preference.PreferenceDialogFragmentCompat} with updates to formatting to match the
+ * project style. This class preserves the {@link DialogPreference.TargetFragment} interface
+ * requirement that was removed in {@link CarUiDialogFragment}. Automotive applications should use
+ * children of this fragment in order to launch the system themed platform {@link AlertDialog}
+ * instead of the one in the support library.
+ */
+public abstract class PreferenceDialogFragment extends CarUiDialogFragment implements
+        DialogInterface.OnClickListener {
+
+    protected static final String ARG_KEY = "key";
+
+    private DialogPreference mPreference;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Fragment rawFragment = getTargetFragment();
+        if (!(rawFragment instanceof DialogPreference.TargetFragment)) {
+            throw new IllegalStateException(
+                    "Target fragment must implement TargetFragment interface");
+        }
+
+        DialogPreference.TargetFragment fragment =
+                (DialogPreference.TargetFragment) rawFragment;
+
+        String key = getArguments().getString(ARG_KEY);
+        if (savedInstanceState == null) {
+            mPreference = (DialogPreference) fragment.findPreference(key);
+            mDialogTitle = mPreference.getDialogTitle();
+            mPositiveButtonText = mPreference.getPositiveButtonText();
+            mNegativeButtonText = mPreference.getNegativeButtonText();
+            mDialogMessage = mPreference.getDialogMessage();
+            mDialogLayoutRes = mPreference.getDialogLayoutResource();
+
+            Drawable icon = mPreference.getDialogIcon();
+            if (icon == null || icon instanceof BitmapDrawable) {
+                mDialogIcon = (BitmapDrawable) icon;
+            } else {
+                Bitmap bitmap = Bitmap.createBitmap(icon.getIntrinsicWidth(),
+                        icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+                Canvas canvas = new Canvas(bitmap);
+                icon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+                icon.draw(canvas);
+                mDialogIcon = new BitmapDrawable(getResources(), bitmap);
+            }
+        }
+    }
+
+    /**
+     * Get the preference that requested this dialog. Available after {@link #onCreate(Bundle)} has
+     * been called on the {@link PreferenceFragmentCompat} which launched this dialog.
+     *
+     * @return the {@link DialogPreference} associated with this dialog.
+     */
+    public DialogPreference getPreference() {
+        if (mPreference == null) {
+            String key = getArguments().getString(ARG_KEY);
+            DialogPreference.TargetFragment fragment =
+                    (DialogPreference.TargetFragment) getTargetFragment();
+            mPreference = (DialogPreference) fragment.findPreference(key);
+        }
+        return mPreference;
+    }
+}
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
new file mode 100644
index 0000000..bff6c13
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/preference/PreferenceFragment.java
@@ -0,0 +1,358 @@
+/*
+ * 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.preference;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.preference.DialogPreference;
+import androidx.preference.DropDownPreference;
+import androidx.preference.EditTextPreference;
+import androidx.preference.ListPreference;
+import androidx.preference.MultiSelectListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
+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;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A PreferenceFragmentCompat is the entry point to using the Preference library.
+ *
+ * <p>Using this fragment will replace regular Preferences with CarUi equivalents. Because of this,
+ * certain properties that cannot be read out of Preferences will be lost upon calling
+ * {@link #setPreferenceScreen(PreferenceScreen)}. These include the preference viewId,
+ * defaultValue, and enabled state.
+ */
+public abstract class PreferenceFragment extends PreferenceFragmentCompat implements
+        InsetsChangedListener {
+
+    private static final String TAG = "CarUiPreferenceFragment";
+    private static final String DIALOG_FRAGMENT_TAG =
+            "com.android.car.ui.PreferenceFragment.DIALOG";
+
+    @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) {
+            return;
+        }
+
+        recyclerView.setPadding(0, toolbar.getHeight(), 0, 0);
+        toolbar.registerToolbarHeightChangeListener(newHeight -> {
+            if (recyclerView.getPaddingTop() == newHeight) {
+                return;
+            }
+
+            int oldHeight = recyclerView.getPaddingTop();
+            recyclerView.setPadding(0, newHeight, 0, 0);
+            recyclerView.scrollBy(0, oldHeight - newHeight);
+        });
+
+        recyclerView.setClipToPadding(false);
+        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);
+    }
+
+    /**
+     * Called when a preference in the tree requests to display a dialog. Subclasses should override
+     * this method to display custom dialogs or to handle dialogs for custom preference classes.
+     *
+     * <p>Note: this is borrowed as-is from androidx.preference.PreferenceFragmentCompat with
+     * updates to launch Car UI library {@link DialogFragment} instead of the ones in the
+     * support library.
+     *
+     * @param preference The {@link Preference} object requesting the dialog
+     */
+    @Override
+    public void onDisplayPreferenceDialog(Preference preference) {
+
+        if (getActivity() instanceof OnPreferenceDisplayDialogCallback
+                && ((OnPreferenceDisplayDialogCallback) getActivity())
+                .onPreferenceDisplayDialog(this, preference)) {
+            return;
+        }
+
+        // check if dialog is already showing
+        if (requireFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
+            return;
+        }
+
+        final Fragment f;
+        if (preference instanceof EditTextPreference) {
+            f = EditTextPreferenceDialogFragment.newInstance(preference.getKey());
+        } else if (preference instanceof ListPreference) {
+            f = ListPreferenceFragment.newInstance(preference.getKey());
+        } else if (preference instanceof MultiSelectListPreference) {
+            f = MultiSelectListPreferenceFragment.newInstance(preference.getKey());
+        } else {
+            throw new IllegalArgumentException(
+                    "Cannot display dialog for an unknown Preference type: "
+                            + preference.getClass().getSimpleName()
+                            + ". Make sure to implement onPreferenceDisplayDialog() to handle "
+                            + "displaying a custom dialog for this Preference.");
+        }
+
+        f.setTargetFragment(this, 0);
+
+        if (f instanceof DialogFragment) {
+            ((DialogFragment) f).show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
+        } else {
+            if (getActivity() == null) {
+                throw new IllegalStateException(
+                        "Preference fragment is not attached to an Activity.");
+            }
+
+            if (getView() == null) {
+                throw new IllegalStateException(
+                        "Preference fragment must have a layout.");
+            }
+
+            Context context = getContext();
+            getActivity().getSupportFragmentManager().beginTransaction()
+                    .setCustomAnimations(
+                            CarUiUtils.getAttrResourceId(context,
+                                    android.R.attr.fragmentOpenEnterAnimation),
+                            CarUiUtils.getAttrResourceId(context,
+                                    android.R.attr.fragmentOpenExitAnimation),
+                            CarUiUtils.getAttrResourceId(context,
+                                    android.R.attr.fragmentCloseEnterAnimation),
+                            CarUiUtils.getAttrResourceId(context,
+                                    android.R.attr.fragmentCloseExitAnimation))
+                    .replace(((ViewGroup) getView().getParent()).getId(), f)
+                    .addToBackStack(null)
+                    .commit();
+        }
+    }
+
+    /**
+     * This override of setPreferenceScreen replaces preferences with their CarUi versions first.
+     */
+    @Override
+    public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
+        // We do a search of the tree and every time we see a PreferenceGroup we remove
+        // all it's children, replace them with CarUi versions, and then re-add them
+
+        Map<Preference, String> dependencies = new HashMap<>();
+        List<Preference> children = new ArrayList<>();
+
+        // Stack of preferences to process
+        Deque<Preference> stack = new ArrayDeque<>();
+        stack.addFirst(preferenceScreen);
+
+        while (!stack.isEmpty()) {
+            Preference preference = stack.removeFirst();
+
+            if (preference instanceof PreferenceGroup) {
+                PreferenceGroup pg = (PreferenceGroup) preference;
+
+                children.clear();
+                for (int i = 0; i < pg.getPreferenceCount(); i++) {
+                    children.add(pg.getPreference(i));
+                }
+
+                pg.removeAll();
+
+                for (Preference child : children) {
+                    Preference replacement = getReplacementFor(child);
+
+                    dependencies.put(replacement, child.getDependency());
+                    pg.addPreference(replacement);
+                    stack.addFirst(replacement);
+                }
+            }
+        }
+
+        super.setPreferenceScreen(preferenceScreen);
+
+        // Set the dependencies after all the swapping has been done and they've been
+        // associated with this fragment, or we could potentially fail to find preferences
+        // or use the wrong preferenceManager
+        for (Map.Entry<Preference, String> entry : dependencies.entrySet()) {
+            entry.getKey().setDependency(entry.getValue());
+        }
+    }
+
+    // Mapping from regular preferences to CarUi preferences.
+    // Order is important, subclasses must come before their base classes
+    private static final List<Pair<Class<? extends Preference>, Class<? extends Preference>>>
+            sPreferenceMapping = Arrays.asList(
+            new Pair<>(DropDownPreference.class, CarUiDropDownPreference.class),
+            new Pair<>(ListPreference.class, CarUiListPreference.class),
+            new Pair<>(MultiSelectListPreference.class, CarUiMultiSelectListPreference.class),
+            new Pair<>(EditTextPreference.class, CarUiEditTextPreference.class),
+            new Pair<>(Preference.class, CarUiPreference.class)
+    );
+
+    /**
+     * Gets the CarUi version of the passed in preference. If there is no suitable replacement, this
+     * method will return it's input.
+     *
+     * <p>When given a Preference that extends a replaceable preference, we log a warning instead
+     * of replacing it so that we don't remove any functionality.
+     */
+    private static Preference getReplacementFor(Preference preference) {
+        Class<? extends Preference> clazz = preference.getClass();
+
+        for (Pair<Class<? extends Preference>, Class<? extends Preference>> replacement
+                : sPreferenceMapping) {
+            Class<? extends Preference> source = replacement.first;
+            Class<? extends Preference> target = replacement.second;
+            if (source.isAssignableFrom(clazz)) {
+                if (clazz == source) {
+                    try {
+                        return copyPreference(preference, (Preference) target
+                                .getDeclaredConstructor(Context.class)
+                                .newInstance(preference.getContext()));
+                    } catch (ReflectiveOperationException e) {
+                        throw new RuntimeException(e);
+                    }
+                } else if (clazz == target || source == Preference.class) {
+                    // Don't warn about subclasses of Preference because there are many legitimate
+                    // uses for non-carui Preference subclasses, like Preference groups.
+                    return preference;
+                } else {
+                    Log.w(TAG, "Subclass of " + source.getSimpleName() + " was used, "
+                            + "preventing us from substituting it with " + target.getSimpleName());
+                    return preference;
+                }
+            }
+        }
+
+        return preference;
+    }
+
+    /**
+     * Copies all the properties of one preference to another.
+     *
+     * @return the {@code to} parameter
+     */
+    private static Preference copyPreference(Preference from, Preference to) {
+        // viewId and defaultValue don't have getters
+        // isEnabled() is not completely symmetrical with setEnabled(), so we can't use it.
+        to.setTitle(from.getTitle());
+        to.setOnPreferenceClickListener(from.getOnPreferenceClickListener());
+        to.setOnPreferenceChangeListener(from.getOnPreferenceChangeListener());
+        to.setIcon(from.getIcon());
+        to.setFragment(from.getFragment());
+        to.setIntent(from.getIntent());
+        to.setKey(from.getKey());
+        to.setOrder(from.getOrder());
+        to.setSelectable(from.isSelectable());
+        to.setPersistent(from.isPersistent());
+        to.setIconSpaceReserved(from.isIconSpaceReserved());
+        to.setWidgetLayoutResource(from.getWidgetLayoutResource());
+        to.setPreferenceDataStore(from.getPreferenceDataStore());
+        to.setShouldDisableView(from.getShouldDisableView());
+        to.setSingleLineTitle(from.isSingleLineTitle());
+        to.setVisible(from.isVisible());
+        to.setLayoutResource(from.getLayoutResource());
+        to.setCopyingEnabled(from.isCopyingEnabled());
+
+        if (from.getSummaryProvider() != null) {
+            to.setSummaryProvider(from.getSummaryProvider());
+        } else {
+            to.setSummary(from.getSummary());
+        }
+
+        if (from.peekExtras() != null) {
+            to.getExtras().putAll(from.peekExtras());
+        }
+
+        if (from instanceof DialogPreference) {
+            DialogPreference fromDialog = (DialogPreference) from;
+            DialogPreference toDialog = (DialogPreference) to;
+            toDialog.setDialogTitle(fromDialog.getDialogTitle());
+            toDialog.setDialogIcon(fromDialog.getDialogIcon());
+            toDialog.setDialogMessage(fromDialog.getDialogMessage());
+            toDialog.setDialogLayoutResource(fromDialog.getDialogLayoutResource());
+            toDialog.setNegativeButtonText(fromDialog.getNegativeButtonText());
+            toDialog.setPositiveButtonText(fromDialog.getPositiveButtonText());
+        }
+
+        // DropDownPreference extends ListPreference and doesn't add any extra api surface,
+        // so we don't need a case for it
+        if (from instanceof ListPreference) {
+            ListPreference fromList = (ListPreference) from;
+            ListPreference toList = (ListPreference) to;
+            toList.setEntries(fromList.getEntries());
+            toList.setEntryValues(fromList.getEntryValues());
+            toList.setValue(fromList.getValue());
+        } else if (from instanceof EditTextPreference) {
+            EditTextPreference fromText = (EditTextPreference) from;
+            EditTextPreference toText = (EditTextPreference) to;
+            toText.setText(fromText.getText());
+        } else if (from instanceof MultiSelectListPreference) {
+            MultiSelectListPreference fromMulti = (MultiSelectListPreference) from;
+            MultiSelectListPreference toMulti = (MultiSelectListPreference) to;
+            toMulti.setEntries(fromMulti.getEntries());
+            toMulti.setEntryValues(fromMulti.getEntryValues());
+            toMulti.setValues(fromMulti.getValues());
+        }
+
+        // We don't need to add checks for things that we will never replace,
+        // like PreferenceGroup or CheckBoxPreference
+
+        return to;
+    }
+}
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
new file mode 100644
index 0000000..3fbe006
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiContentListItem.java
@@ -0,0 +1,352 @@
+/*
+ * 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.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Definition of list items that can be inserted into {@link CarUiListItemAdapter}.
+ */
+public class CarUiContentListItem extends CarUiListItem {
+
+    /**
+     * Callback to be invoked when the checked state of a list item changed.
+     */
+    public interface OnCheckedChangeListener {
+        /**
+         * Called when the checked state of a list item has changed.
+         *
+         * @param item      whose checked state changed.
+         * @param isChecked new checked state of list item.
+         */
+        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
+    }
+
+    /**
+     * Enum of secondary action types of a list item.
+     */
+    public enum Action {
+        /**
+         * For an action value of NONE, no action element is shown for a list item.
+         */
+        NONE,
+        /**
+         * For an action value of SWITCH, a switch is shown for the action element of the list item.
+         */
+        SWITCH,
+        /**
+         * For an action value of CHECK_BOX, a checkbox is shown for the action element of the list
+         * item.
+         */
+        CHECK_BOX,
+        /**
+         * For an action value of RADIO_BUTTON, a radio button is shown for the action element of
+         * the list item.
+         */
+        RADIO_BUTTON,
+        /**
+         * For an action value of ICON, an icon is shown for the action element of the list item.
+         */
+        ICON
+    }
+
+    private Drawable mIcon;
+    @Nullable
+    private Drawable mSupplementalIcon;
+    private CharSequence mTitle;
+    private CharSequence mBody;
+    private Action mAction;
+    private IconType mPrimaryIconType;
+    private boolean mIsActionDividerVisible;
+    private boolean mIsChecked;
+    private boolean mIsEnabled = true;
+    private boolean mIsActivated;
+    private OnClickListener mOnClickListener;
+    private OnCheckedChangeListener mOnCheckedChangeListener;
+    private View.OnClickListener mSupplementalIconOnClickListener;
+
+
+    public CarUiContentListItem(Action action) {
+        mAction = action;
+        mPrimaryIconType = IconType.STANDARD;
+    }
+
+    /**
+     * Returns the title of the item.
+     */
+    @Nullable
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Sets the title of the item.
+     *
+     * @param title text to display as title.
+     */
+    public void setTitle(@NonNull CharSequence title) {
+        mTitle = title;
+    }
+
+    /**
+     * Returns the body text of the item.
+     */
+    @Nullable
+    public CharSequence getBody() {
+        return mBody;
+    }
+
+    /**
+     * Sets the body of the item.
+     *
+     * @param body text to display as body text.
+     */
+    public void setBody(@NonNull CharSequence body) {
+        mBody = body;
+    }
+
+    /**
+     * Returns the icon of the item.
+     */
+    @Nullable
+    public Drawable getIcon() {
+        return mIcon;
+    }
+
+    /**
+     * Sets the icon of the item.
+     *
+     * @param icon the icon to display.
+     */
+    public void setIcon(@Nullable Drawable icon) {
+        mIcon = icon;
+    }
+
+    /**
+     * 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}.
+     */
+    public boolean isChecked() {
+        return mIsChecked;
+    }
+
+    /**
+     * Sets the checked state of the item.
+     *
+     * @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);
+            }
+        }
+    }
+
+    /**
+     * Sets the visibility of the action divider.
+     *
+     * @param visible visibility of the action divider.
+     */
+    public void setActionDividerVisible(boolean visible) {
+        mIsActionDividerVisible = visible;
+    }
+
+    /**
+     * Returns {@code true} if the action divider is visible.
+     */
+    public boolean isActionDividerVisible() {
+        return mIsActionDividerVisible;
+    }
+
+    /**
+     * Returns the action type for the item.
+     */
+    public Action getAction() {
+        return mAction;
+    }
+
+    /**
+     * Returns the supplemental icon for the item.
+     */
+    @Nullable
+    public Drawable getSupplementalIcon() {
+        if (mAction != Action.ICON) {
+            return null;
+        }
+
+        return mSupplementalIcon;
+    }
+
+    /**
+     * Sets supplemental icon to be displayed in a list item.
+     *
+     * @param icon the Drawable to set as the icon, or null to clear the content.
+     */
+    public void setSupplementalIcon(@Nullable Drawable icon) {
+        setSupplementalIcon(icon, null);
+    }
+
+    /**
+     * Sets supplemental icon to be displayed in a list item.
+     *
+     * @param icon     the Drawable to set as the icon, or null to clear the content.
+     * @param listener the callback that is invoked when the icon is clicked.
+     */
+    public void setSupplementalIcon(@Nullable Drawable icon,
+            @Nullable View.OnClickListener listener) {
+        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;
+    }
+
+    @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
+     * {@code Action.CHECK_BOX}.
+     *
+     * @param listener callback to be invoked when the checked state shown in the UI changes.
+     */
+    public void setOnCheckedChangeListener(
+            @Nullable OnCheckedChangeListener listener) {
+        mOnCheckedChangeListener = listener;
+    }
+
+    /**
+     * Returns the {@link OnCheckedChangeListener} registered for this item.
+     */
+    @Nullable
+    public OnCheckedChangeListener getOnCheckedChangeListener() {
+        return mOnCheckedChangeListener;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiHeaderListItem.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiHeaderListItem.java
new file mode 100644
index 0000000..57fd755
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiHeaderListItem.java
@@ -0,0 +1,51 @@
+/*
+ * 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 androidx.annotation.NonNull;
+
+/**
+ * Definition of list item header that can be inserted into {@link CarUiListItemAdapter}.
+ */
+public class CarUiHeaderListItem extends CarUiListItem {
+
+    private CharSequence mTitle;
+    private CharSequence mBody;
+
+    public CarUiHeaderListItem(@NonNull CharSequence title) {
+        this(title, "");
+    }
+
+    public CarUiHeaderListItem(@NonNull CharSequence title, @NonNull CharSequence body) {
+        mTitle = title;
+        mBody = body;
+    }
+
+    /**
+     * Returns the title text for the header.
+     */
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Returns the body text for the header.
+     */
+    public CharSequence getBody() {
+        return mBody;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItem.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItem.java
new file mode 100644
index 0000000..c894774
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItem.java
@@ -0,0 +1,7 @@
+package com.android.car.ui.recyclerview;
+
+/**
+ * All items that can be inserted into {@link CarUiListItemAdapter} must extend this class.
+ */
+public abstract class CarUiListItem {
+}
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
new file mode 100644
index 0000000..65a6d40
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemAdapter.java
@@ -0,0 +1,377 @@
+/*
+ * 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 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;
+
+import java.util.List;
+
+/**
+ * Adapter for {@link CarUiRecyclerView} to display {@link CarUiContentListItem} and {@link
+ * CarUiHeaderListItem}.
+ *
+ * <ul>
+ * <li> Implements {@link CarUiRecyclerView.ItemCap} - defaults to unlimited item count.
+ * </ul>
+ */
+public class CarUiListItemAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements
+        CarUiRecyclerView.ItemCap {
+
+    static final int VIEW_TYPE_LIST_ITEM = 1;
+    static final int VIEW_TYPE_LIST_HEADER = 2;
+
+    private List<? extends CarUiListItem> mItems;
+    private int mMaxItems = CarUiRecyclerView.ItemCap.UNLIMITED;
+
+    public CarUiListItemAdapter(List<? extends CarUiListItem> items) {
+        this.mItems = items;
+    }
+
+    @NonNull
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(
+            @NonNull ViewGroup parent, int viewType) {
+        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+
+        switch (viewType) {
+            case VIEW_TYPE_LIST_ITEM:
+                return new ListItemViewHolder(
+                        inflater.inflate(R.layout.car_ui_list_item, parent, false));
+            case VIEW_TYPE_LIST_HEADER:
+                return new HeaderViewHolder(
+                        inflater.inflate(R.layout.car_ui_header_list_item, parent, false));
+            default:
+                throw new IllegalStateException("Unknown item type.");
+        }
+    }
+
+    /**
+     * Returns the data set held by the adapter.
+     *
+     * <p>Any changes performed to this mutable list must be followed with an invocation of the
+     * appropriate notify method for the adapter.
+     */
+    @NonNull
+    public List<? extends CarUiListItem> getItems() {
+        return mItems;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (mItems.get(position) instanceof CarUiContentListItem) {
+            return VIEW_TYPE_LIST_ITEM;
+        } else if (mItems.get(position) instanceof CarUiHeaderListItem) {
+            return VIEW_TYPE_LIST_HEADER;
+        }
+
+        throw new IllegalStateException("Unknown view type.");
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+        switch (holder.getItemViewType()) {
+            case VIEW_TYPE_LIST_ITEM:
+                if (!(holder instanceof ListItemViewHolder)) {
+                    throw new IllegalStateException("Incorrect view holder type for list item.");
+                }
+
+                CarUiListItem item = mItems.get(position);
+                if (!(item instanceof CarUiContentListItem)) {
+                    throw new IllegalStateException(
+                            "Expected item to be bound to viewholder to be instance of "
+                                    + "CarUiContentListItem.");
+                }
+
+                ((ListItemViewHolder) holder).bind((CarUiContentListItem) item);
+                break;
+            case VIEW_TYPE_LIST_HEADER:
+                if (!(holder instanceof HeaderViewHolder)) {
+                    throw new IllegalStateException("Incorrect view holder type for list item.");
+                }
+
+                CarUiListItem header = mItems.get(position);
+                if (!(header instanceof CarUiHeaderListItem)) {
+                    throw new IllegalStateException(
+                            "Expected item to be bound to viewholder to be instance of "
+                                    + "CarUiHeaderListItem.");
+                }
+
+                ((HeaderViewHolder) holder).bind((CarUiHeaderListItem) header);
+                break;
+            default:
+                throw new IllegalStateException("Unknown item view type.");
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        return mMaxItems == CarUiRecyclerView.ItemCap.UNLIMITED
+                ? mItems.size()
+                : Math.min(mItems.size(), mMaxItems);
+    }
+
+    @Override
+    public void setMaxItems(int maxItems) {
+        mMaxItems = maxItems;
+    }
+
+    /**
+     * Holds views of {@link CarUiContentListItem}.
+     */
+    static class ListItemViewHolder extends RecyclerView.ViewHolder {
+
+        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 = 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);
+        }
+
+        void bind(@NonNull CarUiContentListItem item) {
+            CharSequence title = item.getTitle();
+            CharSequence body = item.getBody();
+            Drawable icon = item.getIcon();
+
+            if (!TextUtils.isEmpty(title)) {
+                mTitle.setText(title);
+                mTitle.setVisibility(View.VISIBLE);
+            } else {
+                mTitle.setVisibility(View.GONE);
+            }
+
+            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) {
+                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);
+
+                    // 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:
+                    bindCompoundButton(item, mSwitch, itemOnClickListener);
+                    break;
+                case CHECK_BOX:
+                    bindCompoundButton(item, mCheckBox, itemOnClickListener);
+                    break;
+                case RADIO_BUTTON:
+                    bindCompoundButton(item, mRadioButton, itemOnClickListener);
+                    break;
+                case ICON:
+                    mSupplementalIcon.setVisibility(View.VISIBLE);
+                    mSupplementalIcon.setImageDrawable(item.getSupplementalIcon());
+                    mActionContainer.setVisibility(View.VISIBLE);
+                    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
+                    // two distinct touch area; the action container and the remainder of the list
+                    // item. Each touch area will have its own ripple effect. If the icon has no
+                    // click listener, it shouldn't be clickable.
+                    if (item.getSupplementalIconOnClickListener() == null) {
+                        mTouchInterceptor.setVisibility(View.VISIBLE);
+                        mTouchInterceptor.setOnClickListener(v -> {
+                            if (itemOnClickListener != null) {
+                                itemOnClickListener.onClick(item);
+                            }
+                        });
+                        mReducedTouchInterceptor.setVisibility(View.GONE);
+                        mActionContainerTouchInterceptor.setVisibility(View.GONE);
+                    } else {
+                        mReducedTouchInterceptor.setVisibility(View.VISIBLE);
+                        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);
+        }
+    }
+
+    /**
+     * Holds views of {@link CarUiHeaderListItem}.
+     */
+    static class HeaderViewHolder extends RecyclerView.ViewHolder {
+
+        private final TextView mTitle;
+        private final TextView mBody;
+
+        HeaderViewHolder(@NonNull View itemView) {
+            super(itemView);
+            mTitle = findViewByRefId(itemView, R.id.title);
+            mBody = findViewByRefId(itemView, R.id.body);
+        }
+
+        private void bind(@NonNull CarUiHeaderListItem item) {
+            mTitle.setText(item.getTitle());
+
+            CharSequence body = item.getBody();
+            if (!TextUtils.isEmpty(body)) {
+                mBody.setText(body);
+            } else {
+                mBody.setVisibility(View.GONE);
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000..dec9080
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -0,0 +1,407 @@
+/*
+ * 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.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.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+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.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+import com.android.car.ui.recyclerview.decorations.grid.GridDividerItemDecoration;
+import com.android.car.ui.recyclerview.decorations.grid.GridOffsetItemDecoration;
+import com.android.car.ui.recyclerview.decorations.linear.LinearDividerItemDecoration;
+import com.android.car.ui.recyclerview.decorations.linear.LinearOffsetItemDecoration;
+import com.android.car.ui.recyclerview.decorations.linear.LinearOffsetItemDecoration.OffsetPosition;
+import com.android.car.ui.toolbar.Toolbar;
+import com.android.car.ui.utils.CarUxRestrictionsUtil;
+
+import java.lang.annotation.Retention;
+
+/**
+ * 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 String TAG = "CarUiRecyclerView";
+
+    private final UxRestrictionChangedListener mListener = new UxRestrictionChangedListener();
+
+    private CarUxRestrictionsUtil mCarUxRestrictionsUtil;
+    private boolean mScrollBarEnabled;
+    private String mScrollBarClass;
+    private boolean mFullyInitialized;
+    private float mScrollBarPaddingStart;
+    private float mScrollBarPaddingEnd;
+
+    private ScrollBar mScrollBar;
+    private int mInitialTopPadding;
+
+    private GridOffsetItemDecoration mOffsetItemDecoration;
+    private GridDividerItemDecoration mDividerItemDecoration;
+    @CarUiRecyclerViewLayout
+    private int mCarUiRecyclerViewLayout;
+    private int mNumOfColumns;
+    private boolean mInstallingExtScrollBar = false;
+    private int mContainerVisibility = View.VISIBLE;
+    private LinearLayout mContainer;
+
+    /**
+     * The possible values for setScrollBarPosition. The default value is actually {@link
+     * CarUiRecyclerViewLayout#LINEAR}.
+     */
+    @IntDef({
+            CarUiRecyclerViewLayout.LINEAR,
+            CarUiRecyclerViewLayout.GRID,
+    })
+    @Retention(SOURCE)
+    public @interface CarUiRecyclerViewLayout {
+        /**
+         * Arranges items either horizontally in a single row or vertically in a single column.
+         * This is default.
+         */
+        int LINEAR = 0;
+
+        /** Arranges items in a Grid. */
+        int GRID = 2;
+    }
+
+    /**
+     * Interface for a {@link RecyclerView.Adapter} to cap the number of items.
+     *
+     * <p>NOTE: it is still up to the adapter to use maxItems in {@link
+     * RecyclerView.Adapter#getItemCount()}.
+     *
+     * <p>the recommended way would be with:
+     *
+     * <pre>{@code
+     * {@literal@}Override
+     * public int getItemCount() {
+     *   return Math.min(super.getItemCount(), mMaxItems);
+     * }
+     * }</pre>
+     */
+    public interface ItemCap {
+
+        /**
+         * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit.
+         */
+        int UNLIMITED = -1;
+
+        /**
+         * Sets the maximum number of items available in the adapter. A value less than '0' means
+         * the
+         * list should not be capped.
+         */
+        void setMaxItems(int maxItems);
+    }
+
+    public CarUiRecyclerView(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public CarUiRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, R.attr.carUiRecyclerViewStyle);
+    }
+
+    public CarUiRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs,
+            int defStyle) {
+        super(context, attrs, defStyle);
+        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,
+                defStyleAttr,
+                R.style.Widget_CarUi_CarUiRecyclerView);
+
+        mScrollBarEnabled = context.getResources().getBoolean(R.bool.car_ui_scrollbar_enable);
+        mFullyInitialized = false;
+
+        mScrollBarPaddingStart =
+                context.getResources().getDimension(R.dimen.car_ui_scrollbar_padding_start);
+        mScrollBarPaddingEnd =
+                context.getResources().getDimension(R.dimen.car_ui_scrollbar_padding_end);
+
+        mCarUiRecyclerViewLayout =
+                a.getInt(R.styleable.CarUiRecyclerView_layoutStyle, CarUiRecyclerViewLayout.LINEAR);
+        mNumOfColumns = a.getInt(R.styleable.CarUiRecyclerView_numOfColumns, /* defValue= */ 2);
+        boolean enableDivider =
+                a.getBoolean(R.styleable.CarUiRecyclerView_enableDivider, /* defValue= */ false);
+
+        if (mCarUiRecyclerViewLayout == CarUiRecyclerViewLayout.LINEAR) {
+
+            int linearTopOffset =
+                    a.getInteger(R.styleable.CarUiRecyclerView_startOffset, /* defValue= */ 0);
+            int linearBottomOffset =
+                    a.getInteger(R.styleable.CarUiRecyclerView_endOffset, /* defValue= */ 0);
+
+            if (enableDivider) {
+                RecyclerView.ItemDecoration dividerItemDecoration =
+                        new LinearDividerItemDecoration(
+                                context.getDrawable(R.drawable.car_ui_recyclerview_divider));
+                addItemDecoration(dividerItemDecoration);
+            }
+            RecyclerView.ItemDecoration topOffsetItemDecoration =
+                    new LinearOffsetItemDecoration(linearTopOffset, OffsetPosition.START);
+
+            RecyclerView.ItemDecoration bottomOffsetItemDecoration =
+                    new LinearOffsetItemDecoration(linearBottomOffset, OffsetPosition.END);
+
+            addItemDecoration(topOffsetItemDecoration);
+            addItemDecoration(bottomOffsetItemDecoration);
+            setLayoutManager(new LinearLayoutManager(getContext()));
+        } else {
+            int gridTopOffset =
+                    a.getInteger(R.styleable.CarUiRecyclerView_startOffset, /* defValue= */ 0);
+            int gridBottomOffset =
+                    a.getInteger(R.styleable.CarUiRecyclerView_endOffset, /* defValue= */ 0);
+
+            if (enableDivider) {
+                mDividerItemDecoration =
+                        new GridDividerItemDecoration(
+                                context.getDrawable(R.drawable.car_ui_divider),
+                                context.getDrawable(R.drawable.car_ui_divider),
+                                mNumOfColumns);
+                addItemDecoration(mDividerItemDecoration);
+            }
+
+            mOffsetItemDecoration =
+                    new GridOffsetItemDecoration(gridTopOffset, mNumOfColumns,
+                            OffsetPosition.START);
+
+            GridOffsetItemDecoration bottomOffsetItemDecoration =
+                    new GridOffsetItemDecoration(gridBottomOffset, mNumOfColumns,
+                            OffsetPosition.END);
+
+            addItemDecoration(mOffsetItemDecoration);
+            addItemDecoration(bottomOffsetItemDecoration);
+            setLayoutManager(new GridLayoutManager(getContext(), mNumOfColumns));
+            setNumOfColumns(mNumOfColumns);
+        }
+
+        if (!mScrollBarEnabled) {
+            a.recycle();
+            mFullyInitialized = true;
+            return;
+        }
+
+        mScrollBarClass = context.getResources().getString(R.string.car_ui_scrollbar_component);
+        a.recycle();
+        this.getViewTreeObserver()
+                .addOnGlobalLayoutListener(() -> {
+                    if (mInitialTopPadding == 0) {
+                        mInitialTopPadding = getPaddingTop();
+                    }
+                    mFullyInitialized = true;
+                });
+    }
+
+    @Override
+    public void onHeightChanged(int height) {
+        setPaddingRelative(getPaddingStart(), mInitialTopPadding + height,
+                getPaddingEnd(), getPaddingBottom());
+    }
+
+    /**
+     * 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.
+     */
+    public void setNumOfColumns(int numberOfColumns) {
+        mNumOfColumns = numberOfColumns;
+        if (mOffsetItemDecoration != null) {
+            mOffsetItemDecoration.setNumOfColumns(mNumOfColumns);
+        }
+        if (mDividerItemDecoration != null) {
+            mDividerItemDecoration.setNumOfColumns(mNumOfColumns);
+        }
+    }
+
+    @Override
+    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);
+        if (mInstallingExtScrollBar || !mScrollBarEnabled) {
+            return;
+        }
+        // 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;
+    }
+
+    /**
+     * 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));
+    }
+
+    private void createScrollBarFromConfig(View scrollView) {
+        Class<?> cls;
+        try {
+            cls = !TextUtils.isEmpty(mScrollBarClass)
+                    ? getContext().getClassLoader().loadClass(mScrollBarClass)
+                    : DefaultScrollBar.class;
+        } catch (Throwable t) {
+            throw andLog("Error loading scroll bar component: " + mScrollBarClass, t);
+        }
+        try {
+            mScrollBar = (ScrollBar) cls.getDeclaredConstructor().newInstance();
+        } catch (Throwable t) {
+            throw andLog("Error creating scroll bar component: " + mScrollBarClass, t);
+        }
+
+        mScrollBar.initialize(this, scrollView);
+
+        mScrollBar.setPadding((int) mScrollBarPaddingStart, (int) mScrollBarPaddingEnd);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mCarUxRestrictionsUtil.unregister(mListener);
+    }
+
+    /**
+     * Sets the scrollbar's padding start (top) and end (bottom).
+     * This padding is applied in addition to the padding of the inner RecyclerView.
+     */
+    public void setScrollBarPadding(int paddingStart, int paddingEnd) {
+        if (mScrollBarEnabled) {
+            mScrollBarPaddingStart = paddingStart;
+            mScrollBarPaddingEnd = paddingEnd;
+
+            if (mScrollBar != null) {
+                mScrollBar.setPadding(paddingStart, paddingEnd);
+            }
+        }
+    }
+
+    /**
+     * @deprecated use {#getLayoutManager()}
+     */
+    @Nullable
+    @Deprecated
+    public LayoutManager getEffectiveLayoutManager() {
+        return super.getLayoutManager();
+    }
+
+    private static RuntimeException andLog(String msg, Throwable t) {
+        Log.e(TAG, msg, t);
+        throw new RuntimeException(msg, t);
+    }
+
+    private class UxRestrictionChangedListener implements
+            CarUxRestrictionsUtil.OnUxRestrictionsChangedListener {
+
+        @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);
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000..5b74b44
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapter.java
@@ -0,0 +1,63 @@
+/*
+ * 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.recyclerview;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+
+/** The adapter for the parent recyclerview in {@link CarUiRecyclerView} widget. */
+final class CarUiRecyclerViewAdapter
+        extends RecyclerView.Adapter<CarUiRecyclerViewAdapter.NestedRowViewHolder> {
+
+    @Override
+    public CarUiRecyclerViewAdapter.NestedRowViewHolder onCreateViewHolder(
+            ViewGroup parent, int viewType) {
+        View v =
+                LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.car_ui_recycler_view_item, parent, false);
+        return new NestedRowViewHolder(v);
+    }
+
+    // Replace the contents of a view (invoked by the layout manager). Intentionally left empty
+    // since this adapter is an empty shell for the nested recyclerview.
+    @Override
+    public void onBindViewHolder(NestedRowViewHolder holder, int position) {
+    }
+
+    // Return the size of your dataset (invoked by the layout manager)
+    @Override
+    public int getItemCount() {
+        return 1;
+    }
+
+    /** The viewholder class for the parent recyclerview. */
+    static class NestedRowViewHolder extends RecyclerView.ViewHolder {
+        public FrameLayout frameLayout;
+
+        NestedRowViewHolder(View view) {
+            super(view);
+            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
new file mode 100644
index 0000000..8ced67a
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
@@ -0,0 +1,100 @@
+/*
+ * 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.recyclerview;
+
+import android.content.Context;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+import com.android.car.ui.utils.CarUiUtils;
+
+/**
+ * Code drop from {androidx.car.widget.CarUiSmoothScroller}
+ *
+ * <p>Custom {@link LinearSmoothScroller} that has:
+ *
+ * <ul>
+ * <li>Custom control over the speed of scrolls.
+ * <li>Scrolling that snaps to start of a child view.
+ * </ul>
+ */
+public class CarUiSmoothScroller extends LinearSmoothScroller {
+    @VisibleForTesting
+    float mMillisecondsPerInch;
+    @VisibleForTesting
+    float mDecelerationTimeDivisor;
+    @VisibleForTesting
+    float mMillisecondsPerPixel;
+    @VisibleForTesting
+    Interpolator mInterpolator;
+    @VisibleForTesting
+    int mDensityDpi;
+
+    public CarUiSmoothScroller(Context context) {
+        super(context);
+        init(context);
+    }
+
+    private void init(Context context) {
+        mMillisecondsPerInch = CarUiUtils.getFloat(context.getResources(),
+                R.dimen.car_ui_scrollbar_milliseconds_per_inch);
+        mDecelerationTimeDivisor = CarUiUtils.getFloat(context.getResources(),
+                R.dimen.car_ui_scrollbar_deceleration_times_divisor);
+        mInterpolator =
+                new DecelerateInterpolator(
+                        CarUiUtils.getFloat(context.getResources(),
+                                R.dimen.car_ui_scrollbar_decelerate_interpolator_factor));
+        mDensityDpi = context.getResources().getDisplayMetrics().densityDpi;
+        mMillisecondsPerPixel = mMillisecondsPerInch / mDensityDpi;
+    }
+
+    @Override
+    protected int getVerticalSnapPreference() {
+        // Returning SNAP_TO_START will ensure that if the top (start) row is partially visible it
+        // will be scrolled downward (END) to make the row fully visible.
+        return SNAP_TO_START;
+    }
+
+    @Override
+    protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+        int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START);
+
+        if (dy == 0) {
+            return;
+        }
+
+        final int time = calculateTimeForDeceleration(dy);
+        if (time > 0) {
+            action.update(0, -dy, time, mInterpolator);
+        }
+    }
+
+    @Override
+    protected int calculateTimeForScrolling(int dx) {
+        return (int) Math.ceil(Math.abs(dx) * mMillisecondsPerPixel);
+    }
+
+    @Override
+    protected int calculateTimeForDeceleration(int dx) {
+        return (int) Math.ceil(calculateTimeForScrolling(dx) / mDecelerationTimeDivisor);
+    }
+}
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
new file mode 100644
index 0000000..807d856
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSnapHelper.java
@@ -0,0 +1,578 @@
+/*
+ * 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.recyclerview;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearSnapHelper;
+import androidx.recyclerview.widget.OrientationHelper;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.LayoutManager;
+
+/**
+ * Inspired by {@link androidx.car.widget.PagedSnapHelper}
+ *
+ * <p>Extension of a {@link LinearSnapHelper} that will snap to the start of the target child view
+ * to the start of the attached {@link RecyclerView}. The start of the view is defined as the top if
+ * the RecyclerView is scrolling vertically; it is defined as the left (or right if RTL) if the
+ * 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;
+
+    public CarUiSnapHelper(Context context) {
+        mContext = context;
+    }
+
+    // Orientation helpers are lazily created per LayoutManager.
+    @Nullable
+    private OrientationHelper mVerticalHelper;
+    @Nullable
+    private OrientationHelper mHorizontalHelper;
+
+    @Override
+    public int[] calculateDistanceToFinalSnap(
+            @NonNull LayoutManager layoutManager, @NonNull View targetView) {
+        int[] out = new int[2];
+        if (layoutManager.canScrollHorizontally()) {
+            out[0] = distanceToTopMargin(targetView, getHorizontalHelper(layoutManager));
+        }
+
+        if (layoutManager.canScrollVertically()) {
+            out[1] = distanceToTopMargin(targetView, getVerticalHelper(layoutManager));
+        }
+
+        return out;
+    }
+
+    /**
+     * Smooth scrolls the RecyclerView by a given distance.
+     */
+    public void smoothScrollBy(int scrollDistance) {
+        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+        if (layoutManager == null) {
+            return;
+        }
+
+        int position = findTargetSnapPosition(layoutManager, scrollDistance);
+        if (position == RecyclerView.NO_POSITION) {
+            mRecyclerView.smoothScrollBy(0, scrollDistance);
+            return;
+        }
+
+        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}
+     */
+    private int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager,
+            int scrollDistance) {
+
+        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        final int itemCount = layoutManager.getItemCount();
+        if (itemCount == 0) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        final View currentView = findViewIfScrollable(layoutManager);
+        if (currentView == null) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        final int currentPosition = layoutManager.getPosition(currentView);
+        if (currentPosition == RecyclerView.NO_POSITION) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
+                (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
+        // deltaJumps sign comes from the velocity which may not match the order of children in
+        // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
+        // get the direction.
+        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
+        if (vectorForEnd == null) {
+            // cannot get a vector for the given position.
+            return RecyclerView.NO_POSITION;
+        }
+
+        int vDeltaJump;
+        int hDeltaJump;
+        if (layoutManager.canScrollHorizontally()) {
+            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
+                    getHorizontalHelper(layoutManager), scrollDistance);
+            if (vectorForEnd.x < 0) {
+                hDeltaJump = -hDeltaJump;
+            }
+        } else {
+            hDeltaJump = 0;
+        }
+        if (layoutManager.canScrollVertically()) {
+            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
+                    getVerticalHelper(layoutManager), scrollDistance);
+            if (vectorForEnd.y < 0) {
+                vDeltaJump = -vDeltaJump;
+            }
+        } else {
+            vDeltaJump = 0;
+        }
+
+        int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
+        if (deltaJump == 0) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        int targetPos = currentPosition + deltaJump;
+        if (targetPos < 0) {
+            targetPos = 0;
+        }
+        if (targetPos >= itemCount) {
+            targetPos = itemCount - 1;
+        }
+        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) {
+        int childCount = layoutManager.getChildCount();
+        if (childCount == 0) {
+            return null;
+        }
+
+        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) {
+        if (layoutManager.canScrollVertically()) {
+            return findTopView(layoutManager, getVerticalHelper(layoutManager));
+        } else if (layoutManager.canScrollHorizontally()) {
+            return findTopView(layoutManager, getHorizontalHelper(layoutManager));
+        }
+        return null;
+    }
+
+    private static int distanceToTopMargin(@NonNull View targetView, OrientationHelper helper) {
+        final int childTop = helper.getDecoratedStart(targetView);
+        final int containerTop = helper.getStartAfterPadding();
+        return childTop - containerTop;
+    }
+
+    /**
+     * 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 RecyclerView.LayoutManager} for the attached
+     *                      RecyclerView.
+     * @return The View closest to the start of the RecyclerView.
+     */
+    private static View findTopView(LayoutManager layoutManager, OrientationHelper helper) {
+        int childCount = layoutManager.getChildCount();
+        if (childCount == 0) {
+            return null;
+        }
+
+        View closestChild = null;
+        int absClosest = Integer.MAX_VALUE;
+
+        for (int i = 0; i < childCount; i++) {
+            View child = layoutManager.getChildAt(i);
+            if (child == null) {
+                continue;
+            }
+            int absDistance = Math.abs(distanceToTopMargin(child, helper));
+
+            /** if child top is closer than previous closest, set it as closest */
+            if (absDistance < absClosest) {
+                absClosest = absDistance;
+                closestChild = child;
+            }
+        }
+        return closestChild;
+    }
+
+    /**
+     * 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 helper An {@link OrientationHelper} to aid with calculation.
+     * @return A float indicating the percentage of the given view that is visible.
+     */
+    private float getPercentageVisible(View view, OrientationHelper helper) {
+        int start = helper.getStartAfterPadding();
+        int end = helper.getEndAfterPadding();
+
+        int viewStart = helper.getDecoratedStart(view);
+        int viewEnd = helper.getDecoratedEnd(view);
+
+        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 ((float) end - start) / helper.getDecoratedMeasurement(view);
+        } else if (viewStart < start) {
+            // 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.
+            return ((float) end - viewStart) / helper.getDecoratedMeasurement(view);
+        }
+    }
+
+    @Override
+    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
+        super.attachToRecyclerView(recyclerView);
+        mRecyclerView = recyclerView;
+    }
+
+    /**
+     * Returns a scroller specific to this {@code PagedSnapHelper}. This scroller is used for all
+     * smooth scrolling operations, including flings.
+     *
+     * @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(@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.
+     *
+     * @param velocityX Fling velocity on the horizontal axis.
+     * @param velocityY Fling velocity on the vertical axis.
+     * @return An array holding the calculated distances in x and y directions respectively.
+     */
+    @Override
+    public int[] calculateScrollDistance(int velocityX, int velocityY) {
+        int[] outDist = super.calculateScrollDistance(velocityX, velocityY);
+
+        if (mRecyclerView == null) {
+            return outDist;
+        }
+
+        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+        if (layoutManager == null || layoutManager.getChildCount() == 0) {
+            return outDist;
+        }
+
+        int lastChildPosition = isAtEnd(layoutManager) ? 0 : layoutManager.getChildCount() - 1;
+
+        OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
+        View lastChild = layoutManager.getChildAt(lastChildPosition);
+        float percentageVisible = getPercentageVisible(lastChild, orientationHelper);
+
+        int maxDistance = layoutManager.getHeight();
+        if (percentageVisible > 0.f) {
+            // The max and min distance is the total height of the RecyclerView minus the height of
+            // the last child. This ensures that each scroll will never scroll more than a single
+            // page on the RecyclerView. That is, the max scroll will make the last child the
+            // first child and vice versa when scrolling the opposite way.
+            maxDistance -= layoutManager.getDecoratedMeasuredHeight(lastChild);
+        }
+
+        int minDistance = -maxDistance;
+
+        outDist[0] = clamp(outDist[0], minDistance, maxDistance);
+        outDist[1] = clamp(outDist[1], minDistance, maxDistance);
+
+        return outDist;
+    }
+
+    /**
+     * 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)
+                        : 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;
+    }
+
+    /**
+     * 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)
+                        : getHorizontalHelper(layoutManager);
+
+        View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
+
+        // The list has reached the bottom if the last child that is visible is the last item
+        // in the list and it's fully shown.
+        return layoutManager.getPosition(lastVisibleChild) == (layoutManager.getItemCount() - 1)
+                && layoutManager.getDecoratedBottom(lastVisibleChild)
+                <= orientationHelper.getEndAfterPadding();
+    }
+
+    /**
+     * Returns an {@link OrientationHelper} that corresponds to the current scroll direction of the
+     * given {@link LayoutManager}.
+     */
+    @NonNull
+    private OrientationHelper getOrientationHelper(@NonNull LayoutManager layoutManager) {
+        return layoutManager.canScrollVertically()
+                ? getVerticalHelper(layoutManager)
+                : getHorizontalHelper(layoutManager);
+    }
+
+    @NonNull
+    private OrientationHelper getVerticalHelper(@NonNull LayoutManager layoutManager) {
+        if (mVerticalHelper == null || mVerticalHelper.getLayoutManager() != layoutManager) {
+            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
+        }
+        return mVerticalHelper;
+    }
+
+    @NonNull
+    private OrientationHelper getHorizontalHelper(@NonNull LayoutManager layoutManager) {
+        if (mHorizontalHelper == null || mHorizontalHelper.getLayoutManager() != layoutManager) {
+            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
+        }
+        return mHorizontalHelper;
+    }
+
+    /**
+     * Ensures that the given value falls between the range given by the min and max values. This
+     * method does not check that the min value is greater than or equal to the max value. If the
+     * 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.
+     * @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.
+     */
+    private static int clamp(int value, int min, int max) {
+        return Math.max(min, Math.min(max, value));
+    }
+
+    private static int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
+            OrientationHelper helper,
+            int scrollDistance) {
+        int[] distances = new int[]{scrollDistance, scrollDistance};
+        float distancePerChild = computeDistancePerChild(layoutManager, helper);
+
+        if (distancePerChild <= 0) {
+            return 0;
+        }
+        int distance =
+                Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
+        return (int) Math.round(distance / distancePerChild);
+    }
+
+    private static float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
+            OrientationHelper helper) {
+        View minPosView = null;
+        View maxPosView = null;
+        int minPos = Integer.MAX_VALUE;
+        int maxPos = Integer.MIN_VALUE;
+        int childCount = layoutManager.getChildCount();
+        if (childCount == 0) {
+            return -1;
+        }
+
+        for (int i = 0; i < childCount; i++) {
+            View child = layoutManager.getChildAt(i);
+            int pos = layoutManager.getPosition(child);
+            if (pos == RecyclerView.NO_POSITION) {
+                continue;
+            }
+            if (pos < minPos) {
+                minPos = pos;
+                minPosView = child;
+            }
+            if (pos > maxPos) {
+                maxPos = pos;
+                maxPosView = child;
+            }
+        }
+        if (minPosView == null || maxPosView == null) {
+            return -1;
+        }
+        int start = Math.min(helper.getDecoratedStart(minPosView),
+                helper.getDecoratedStart(maxPosView));
+        int end = Math.max(helper.getDecoratedEnd(minPosView),
+                helper.getDecoratedEnd(maxPosView));
+        int distance = end - start;
+        if (distance == 0) {
+            return -1;
+        }
+        return 1f * distance / ((maxPos - minPos) + 1);
+    }
+}
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
new file mode 100644
index 0000000..7b38504
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
@@ -0,0 +1,541 @@
+/*
+ * 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.recyclerview;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.content.res.Resources;
+import android.os.Handler;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.widget.OrientationHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+import com.android.car.ui.utils.CarUiUtils;
+
+/**
+ * The default scroll bar widget for the {@link CarUiRecyclerView}.
+ *
+ * <p>Inspired by {@link androidx.car.widget.PagedListView}. Most pagination and scrolling logic has
+ * been ported from the PLV with minor updates.
+ */
+class DefaultScrollBar implements ScrollBar {
+
+    @VisibleForTesting
+    int mPaddingStart;
+    @VisibleForTesting
+    int mPaddingEnd;
+
+    private float mButtonDisabledAlpha;
+    private CarUiSnapHelper mSnapHelper;
+
+    private ImageView mUpButton;
+    private View mScrollView;
+    private View mScrollThumb;
+    private ImageView mDownButton;
+
+    private int mSeparatingMargin;
+
+    private RecyclerView mRecyclerView;
+
+    /** The amount of space that the scroll thumb is allowed to roam over. */
+    private int mScrollThumbTrackHeight;
+
+    private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator();
+
+    private final int mRowsPerPage = -1;
+    private final Handler mHandler = new Handler();
+
+    private OrientationHelper mOrientationHelper;
+
+    @Override
+    public void initialize(RecyclerView rv, View scrollView) {
+        mRecyclerView = rv;
+
+        mScrollView = scrollView;
+
+        Resources res = rv.getContext().getResources();
+
+        mButtonDisabledAlpha = CarUiUtils.getFloat(res, R.dimen.car_ui_button_disabled_alpha);
+
+        getRecyclerView().addOnScrollListener(mRecyclerViewOnScrollListener);
+        getRecyclerView().getRecycledViewPool().setMaxRecycledViews(0, 12);
+
+        mSeparatingMargin = res.getDimensionPixelSize(R.dimen.car_ui_scrollbar_separator_margin);
+
+        mUpButton = requireViewByRefId(mScrollView, R.id.page_up);
+        PaginateButtonClickListener upButtonClickListener =
+                new PaginateButtonClickListener(PaginationListener.PAGE_UP);
+        mUpButton.setOnClickListener(upButtonClickListener);
+
+        mDownButton = requireViewByRefId(mScrollView, R.id.page_down);
+        PaginateButtonClickListener downButtonClickListener =
+                new PaginateButtonClickListener(PaginationListener.PAGE_DOWN);
+        mDownButton.setOnClickListener(downButtonClickListener);
+
+        mScrollThumb = requireViewByRefId(mScrollView, R.id.scrollbar_thumb);
+
+        mSnapHelper = new CarUiSnapHelper(rv.getContext());
+        getRecyclerView().setOnFlingListener(null);
+        mSnapHelper.attachToRecyclerView(getRecyclerView());
+
+        mScrollView.addOnLayoutChangeListener(
+                (View v,
+                        int left,
+                        int top,
+                        int right,
+                        int bottom,
+                        int oldLeft,
+                        int oldTop,
+                        int oldRight,
+                        int oldBottom) -> {
+                    int width = right - left;
+
+                    OrientationHelper orientationHelper =
+                            getOrientationHelper(getRecyclerView().getLayoutManager());
+
+                    // This value will keep track of the top of the current view being laid out.
+                    int layoutTop = orientationHelper.getStartAfterPadding() + mPaddingStart;
+
+                    // Lay out the up button at the top of the view.
+                    layoutViewCenteredFromTop(mUpButton, layoutTop, width);
+                    layoutTop = mUpButton.getBottom();
+
+                    // Lay out the scroll thumb
+                    layoutTop += mSeparatingMargin;
+                    layoutViewCenteredFromTop(mScrollThumb, layoutTop, width);
+
+                    // Lay out the bottom button at the bottom of the view.
+                    int downBottom = orientationHelper.getEndAfterPadding() - mPaddingEnd;
+                    layoutViewCenteredFromBottom(mDownButton, downBottom, width);
+
+                    mHandler.post(this::calculateScrollThumbTrackHeight);
+                    mHandler.post(() -> updatePaginationButtons(/* animate= */ false));
+                });
+    }
+
+    public RecyclerView getRecyclerView() {
+        return mRecyclerView;
+    }
+
+    @Override
+    public void requestLayout() {
+        mScrollView.requestLayout();
+    }
+
+    @Override
+    public void setPadding(int paddingStart, int paddingEnd) {
+        this.mPaddingStart = paddingStart;
+        this.mPaddingEnd = paddingEnd;
+        requestLayout();
+    }
+
+    /**
+     * Sets whether or not the up button on the scroll bar is clickable.
+     *
+     * @param enabled {@code true} if the up button is enabled.
+     */
+    private void setUpEnabled(boolean enabled) {
+        mUpButton.setEnabled(enabled);
+        mUpButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha);
+    }
+
+    /**
+     * Sets whether or not the down button on the scroll bar is clickable.
+     *
+     * @param enabled {@code true} if the down button is enabled.
+     */
+    private void setDownEnabled(boolean enabled) {
+        mDownButton.setEnabled(enabled);
+        mDownButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha);
+    }
+
+    /**
+     * Returns whether or not the down button on the scroll bar is clickable.
+     *
+     * @return {@code true} if the down button is enabled. {@code false} otherwise.
+     */
+    private boolean isDownEnabled() {
+        return mDownButton.isEnabled();
+    }
+
+    /** Listener for when the list should paginate. */
+    interface PaginationListener {
+        int PAGE_UP = 0;
+        int PAGE_DOWN = 1;
+
+        /** Called when the linked view should be paged in the given direction */
+        void onPaginate(int direction);
+    }
+
+    /**
+     * Calculate the amount of space that the scroll bar thumb is allowed to roam. The thumb is
+     * allowed to take up the space between the down bottom and the up or alpha jump button,
+     * depending
+     * on if the latter is visible.
+     */
+    private void calculateScrollThumbTrackHeight() {
+        // Subtracting (2 * mSeparatingMargin) for the top/bottom margin above and below the
+        // scroll bar thumb.
+        mScrollThumbTrackHeight = mDownButton.getTop() - (2 * mSeparatingMargin);
+
+        // If there's an alpha jump button, then the thumb is laid out starting from below that.
+        mScrollThumbTrackHeight -= mUpButton.getBottom();
+    }
+
+    /**
+     * Lays out the given View starting from the given {@code top} value downwards and centered
+     * within the given {@code availableWidth}.
+     *
+     * @param view The view to lay out.
+     * @param top The top value to start laying out from. This value will be the resulting top value
+     * of the view.
+     * @param availableWidth The width in which to center the given view.
+     */
+    private static void layoutViewCenteredFromTop(View view, int top, int availableWidth) {
+        int viewWidth = view.getMeasuredWidth();
+        int viewLeft = (availableWidth - viewWidth) / 2;
+        view.layout(viewLeft, top, viewLeft + viewWidth, top + view.getMeasuredHeight());
+    }
+
+    /**
+     * Lays out the given View starting from the given {@code bottom} value upwards and centered
+     * within the given {@code availableSpace}.
+     *
+     * @param view The view to lay out.
+     * @param bottom The bottom value to start laying out from. This value will be the resulting
+     * bottom value of the view.
+     * @param availableWidth The width in which to center the given view.
+     */
+    private static void layoutViewCenteredFromBottom(View view, int bottom, int availableWidth) {
+        int viewWidth = view.getMeasuredWidth();
+        int viewLeft = (availableWidth - viewWidth) / 2;
+        view.layout(viewLeft, bottom - view.getMeasuredHeight(), viewLeft + viewWidth, bottom);
+    }
+
+    /**
+     * Sets the range, offset and extent of the scroll bar. The range represents the size of a
+     * container for the scrollbar thumb; offset is the distance from the start of the container to
+     * where the thumb should be; and finally, extent is the size of the thumb.
+     *
+     * <p>These values can be expressed in arbitrary units, so long as they share the same units.
+     * The
+     * values should also be positive.
+     *
+     * @param range The range of the scrollbar's thumb
+     * @param offset The offset of the scrollbar's thumb
+     * @param extent The extent of the scrollbar's thumb
+     * @param animate Whether or not the thumb should animate from its current position to the
+     * position specified by the given range, offset and extent.
+     */
+    private void setParameters(
+            @IntRange(from = 0) int range,
+            @IntRange(from = 0) int offset,
+            @IntRange(from = 0) int extent,
+            boolean animate) {
+        // Not laid out yet, so values cannot be calculated.
+        if (!mScrollView.isLaidOut()) {
+            return;
+        }
+
+        // If the scroll bars aren't visible, then no need to update.
+        if (mScrollView.getVisibility() == View.GONE || range == 0) {
+            return;
+        }
+
+        int thumbLength = calculateScrollThumbLength(range, extent);
+        int thumbOffset = calculateScrollThumbOffset(range, offset, thumbLength);
+
+        // Sets the size of the thumb and request a redraw if needed.
+        ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
+
+        if (lp.height != thumbLength) {
+            lp.height = thumbLength;
+            mScrollThumb.requestLayout();
+        }
+
+        moveY(mScrollThumb, thumbOffset, animate);
+    }
+
+    /**
+     * Calculates and returns how big the scroll bar thumb should be based on the given range and
+     * extent.
+     *
+     * @param range The total amount of space the scroll bar is allowed to roam over.
+     * @param extent The amount of space that the scroll bar takes up relative to the range.
+     * @return The height of the scroll bar thumb in pixels.
+     */
+    private int calculateScrollThumbLength(int range, int extent) {
+        // Scale the length by the available space that the thumb can fill.
+        return Math.round(((float) extent / range) * mScrollThumbTrackHeight);
+    }
+
+    /**
+     * Calculates and returns how much the scroll thumb should be offset from the top of where it
+     * has
+     * been laid out.
+     *
+     * @param range The total amount of space the scroll bar is allowed to roam over.
+     * @param offset The amount the scroll bar should be offset, expressed in the same units as the
+     * given range.
+     * @param thumbLength The current length of the thumb in pixels.
+     * @return The amount the thumb should be offset in pixels.
+     */
+    private int calculateScrollThumbOffset(int range, int offset, int thumbLength) {
+        // Ensure that if the user has reached the bottom of the list, then the scroll bar is
+        // aligned to the bottom as well. Otherwise, scale the offset appropriately.
+        // This offset will be a value relative to the parent of this scrollbar, so start by where
+        // the top of mScrollThumb is.
+        return mScrollThumb.getTop()
+                + (isDownEnabled()
+                ? Math.round(((float) offset / range) * mScrollThumbTrackHeight)
+                : mScrollThumbTrackHeight - thumbLength);
+    }
+
+    /** Moves the given view to the specified 'y' position. */
+    private void moveY(final View view, float newPosition, boolean animate) {
+        final int duration = animate ? 200 : 0;
+        view.animate()
+                .y(newPosition)
+                .setDuration(duration)
+                .setInterpolator(mPaginationInterpolator)
+                .start();
+    }
+
+    private class PaginateButtonClickListener implements View.OnClickListener {
+        private final int mPaginateDirection;
+        private PaginationListener mPaginationListener;
+
+        PaginateButtonClickListener(int paginateDirection) {
+            this.mPaginateDirection = paginateDirection;
+        }
+
+        @Override
+        public void onClick(View v) {
+            if (mPaginationListener != null) {
+                mPaginationListener.onPaginate(mPaginateDirection);
+            }
+            if (mPaginateDirection == PaginationListener.PAGE_DOWN) {
+                pageDown();
+            } else if (mPaginateDirection == PaginationListener.PAGE_UP) {
+                pageUp();
+            }
+        }
+    }
+
+    private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener =
+            new RecyclerView.OnScrollListener() {
+                @Override
+                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                    updatePaginationButtons(false);
+                }
+            };
+
+    /** Returns the page the given position is on, starting with page 0. */
+    int getPage(int position) {
+        if (mRowsPerPage == -1) {
+            return -1;
+        }
+        if (mRowsPerPage == 0) {
+            return 0;
+        }
+        return position / mRowsPerPage;
+    }
+
+    private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) {
+        if (mOrientationHelper == null || mOrientationHelper.getLayoutManager() != layoutManager) {
+            // CarUiRecyclerView is assumed to be a list that always vertically scrolls.
+            mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager);
+        }
+        return mOrientationHelper;
+    }
+
+    /**
+     * Scrolls the contents of the RecyclerView up a page. A page is defined as the height of the
+     * {@code CarUiRecyclerView}.
+     *
+     * <p>The resulting first item in the list will be snapped to so that it is completely visible.
+     * If
+     * this is not possible due to the first item being taller than the containing {@code
+     * CarUiRecyclerView}, then the snapping will not occur.
+     */
+    void pageUp() {
+        int currentOffset = getRecyclerView().computeVerticalScrollOffset();
+        if (getRecyclerView().getLayoutManager() == null
+                || getRecyclerView().getChildCount() == 0
+                || currentOffset == 0) {
+            return;
+        }
+
+        // Use OrientationHelper to calculate scroll distance in order to match snapping behavior.
+        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.
+        for (int i = 0; i < getRecyclerView().getChildCount(); i++) {
+            /*
+             * We treat child View longer than screen size differently:
+             * 1) When it enters screen, next pageUp will align its bottom with parent bottom;
+             * 2) When it leaves screen, next pageUp will align its top with parent top.
+             */
+            View child = getRecyclerView().getChildAt(i);
+            if (child.getHeight() > screenSize) {
+                if (orientationHelper.getDecoratedEnd(child) < screenSize) {
+                    // Child view bottom is entering screen. Align its bottom with parent bottom.
+                    scrollDistance = screenSize - orientationHelper.getDecoratedEnd(child);
+                } else if (-screenSize < orientationHelper.getDecoratedStart(child)
+                        && orientationHelper.getDecoratedStart(child) < 0) {
+                    // Child view top is about to enter screen - its distance to parent top
+                    // is less than a full scroll. Align child top with parent top.
+                    scrollDistance = Math.abs(orientationHelper.getDecoratedStart(child));
+                }
+                // There can be two items that are longer than the screen. We stop at the first one.
+                // This is affected by the iteration order.
+                break;
+            }
+        }
+        // Distance should always be positive. Negate its value to scroll up.
+        mSnapHelper.smoothScrollBy(-scrollDistance);
+    }
+
+    /**
+     * Scrolls the contents of the RecyclerView down a page. A page is defined as the height of the
+     * {@code CarUiRecyclerView}.
+     *
+     * <p>This method will attempt to bring the last item in the list as the first item. If the
+     * current first item in the list is taller than the {@code CarUiRecyclerView}, then it will be
+     * scrolled the length of a page, but not snapped to.
+     */
+    void pageDown() {
+        if (getRecyclerView().getLayoutManager() == null
+                || getRecyclerView().getChildCount() == 0) {
+            return;
+        }
+
+        OrientationHelper orientationHelper =
+                getOrientationHelper(getRecyclerView().getLayoutManager());
+        int screenSize = orientationHelper.getTotalSpace();
+        int scrollDistance = screenSize;
+
+        // 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)) {
+            scrollDistance = orientationHelper.getDecoratedStart(lastChild);
+            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;
+            }
+        }
+
+        // The iteration order matters. In case where there are 2 items longer than screen size, we
+        // want to focus on upcoming view (the one at the bottom of screen).
+        for (int i = getRecyclerView().getChildCount() - 1; i >= 0; i--) {
+            /* We treat child View longer than screen size differently:
+             * 1) When it enters screen, next pageDown will align its top with parent top;
+             * 2) When it leaves screen, next pageDown will align its bottom with parent bottom.
+             */
+            View child = getRecyclerView().getChildAt(i);
+            if (child.getHeight() > screenSize) {
+                if (orientationHelper.getDecoratedStart(child) > 0) {
+                    // Child view top is entering screen. Align its top with parent top.
+                    scrollDistance = orientationHelper.getDecoratedStart(child);
+                } else if (screenSize < orientationHelper.getDecoratedEnd(child)
+                        && orientationHelper.getDecoratedEnd(child) < 2 * screenSize) {
+                    // Child view bottom is about to enter screen - its distance to parent bottom
+                    // is less than a full scroll. Align child bottom with parent bottom.
+                    scrollDistance = orientationHelper.getDecoratedEnd(child) - screenSize;
+                }
+                // There can be two items that are longer than the screen. We stop at the first one.
+                // This is affected by the iteration order.
+                break;
+            }
+        }
+
+        mSnapHelper.smoothScrollBy(scrollDistance);
+    }
+
+    /**
+     * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is
+     * being called as a result of adapter changes, it should be called after the new layout has
+     * been
+     * calculated because the method of determining scrollbar visibility uses the current layout.
+     * If
+     * this is called after an adapter change but before the new layout, the visibility
+     * determination
+     * may not be correct.
+     *
+     * @param animate {@code true} if the scrollbar should animate to its new position. {@code
+     * false}
+     * if no animation is used
+     */
+    private void updatePaginationButtons(boolean animate) {
+
+        boolean isAtStart = isAtStart();
+        boolean isAtEnd = isAtEnd();
+        RecyclerView.LayoutManager layoutManager = getRecyclerView().getLayoutManager();
+
+        if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
+            mScrollView.setVisibility(View.INVISIBLE);
+        } else {
+            mScrollView.setVisibility(View.VISIBLE);
+        }
+        setUpEnabled(!isAtStart);
+        setDownEnabled(!isAtEnd);
+
+        if (layoutManager == null) {
+            return;
+        }
+
+        if (layoutManager.canScrollVertically()) {
+            setParameters(
+                    getRecyclerView().computeVerticalScrollRange(),
+                    getRecyclerView().computeVerticalScrollOffset(),
+                    getRecyclerView().computeVerticalScrollExtent(),
+                    animate);
+        } else {
+            setParameters(
+                    getRecyclerView().computeHorizontalScrollRange(),
+                    getRecyclerView().computeHorizontalScrollOffset(),
+                    getRecyclerView().computeHorizontalScrollExtent(),
+                    animate);
+        }
+
+        mScrollView.invalidate();
+    }
+
+    /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
+    boolean isAtStart() {
+        return mSnapHelper.isAtStart(getRecyclerView().getLayoutManager());
+    }
+
+    /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
+    boolean isAtEnd() {
+        return mSnapHelper.isAtEnd(getRecyclerView().getLayoutManager());
+    }
+}
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
new file mode 100644
index 0000000..ce58897
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/ScrollBar.java
@@ -0,0 +1,42 @@
+/*
+ * 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.recyclerview;
+
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * An abstract class that defines required contract for a custom scroll bar for the {@link
+ * CarUiRecyclerView}. All custom scroll bar must inherit from this class.
+ */
+public interface ScrollBar {
+    /**
+     * The concrete class should implement this method to initialize configuration of a scrollbar
+     * view.
+     */
+    void initialize(RecyclerView recyclerView, View scrollView);
+
+    /**
+     * Requests layout of the scrollbar. Should be called when there's been a change that will
+     * affect
+     * the size of the scrollbar view.
+     */
+    void requestLayout();
+
+    /** Sets the padding of the scrollbar, relative to the padding of the RecyclerView. */
+    void setPadding(int padddingStart, int paddingEnd);
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/grid/GridDividerItemDecoration.java b/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/grid/GridDividerItemDecoration.java
new file mode 100644
index 0000000..d462b92
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/grid/GridDividerItemDecoration.java
@@ -0,0 +1,161 @@
+/*
+ * 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.recyclerview.decorations.grid;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+
+/** Adds interior dividers to a RecyclerView with a GridLayoutManager. */
+public class GridDividerItemDecoration extends RecyclerView.ItemDecoration {
+
+    private final Drawable mHorizontalDivider;
+    private final Drawable mVerticalDivider;
+    private int mNumColumns;
+
+    /**
+     * Sole constructor. Takes in {@link Drawable} objects to be used as horizontal and vertical
+     * dividers.
+     *
+     * @param horizontalDivider A divider {@code Drawable} to be drawn on the rows of the grid of
+     * the
+     * RecyclerView
+     * @param verticalDivider A divider {@code Drawable} to be drawn on the columns of the grid of
+     * the
+     * RecyclerView
+     * @param numColumns The number of columns in the grid of the RecyclerView
+     */
+    public GridDividerItemDecoration(
+            Drawable horizontalDivider, Drawable verticalDivider, int numColumns) {
+        this.mHorizontalDivider = horizontalDivider;
+        this.mVerticalDivider = verticalDivider;
+        this.mNumColumns = numColumns;
+    }
+
+    /**
+     * Draws horizontal and/or vertical dividers onto the parent RecyclerView.
+     *
+     * @param canvas The {@link Canvas} onto which dividers will be drawn
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
+        drawVerticalDividers(canvas, parent);
+        drawHorizontalDividers(canvas, parent);
+    }
+
+    /**
+     * Determines the size and location of offsets between items in the parent RecyclerView.
+     *
+     * @param outRect The {@link Rect} of offsets to be added around the child view
+     * @param view The child view to be decorated with an offset
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void getItemOffsets(
+            Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        outRect.set(
+                0, 0, mHorizontalDivider.getIntrinsicWidth(),
+                mHorizontalDivider.getIntrinsicHeight());
+    }
+
+    /**
+     * Adds horizontal dividers to a RecyclerView with a GridLayoutManager or its subclass.
+     *
+     * @param canvas The {@link Canvas} onto which dividers will be drawn
+     * @param parent The RecyclerView onto which dividers are being added
+     */
+    private void drawHorizontalDividers(Canvas canvas, RecyclerView parent) {
+        int childCount = parent.getChildCount();
+        int rowCount = childCount / mNumColumns;
+        int lastRowChildCount = childCount % mNumColumns;
+        int lastColumn = Math.min(childCount, mNumColumns);
+
+        for (int i = 1; i < lastColumn; i++) {
+            int lastRowChildIndex;
+            if (i < lastRowChildCount) {
+                lastRowChildIndex = i + (rowCount * mNumColumns);
+            } else {
+                lastRowChildIndex = i + ((rowCount - 1) * mNumColumns);
+            }
+
+            View firstRowChild = parent.getChildAt(i);
+            View lastRowChild = parent.getChildAt(lastRowChildIndex);
+
+            int dividerTop =
+                    firstRowChild.getTop() + (int) parent.getContext().getResources().getDimension(
+                            R.dimen.car_ui_recyclerview_divider_top_margin);
+            int dividerRight = firstRowChild.getLeft();
+            int dividerLeft = dividerRight - mHorizontalDivider.getIntrinsicWidth();
+            int dividerBottom = lastRowChild.getBottom()
+                    - (int) parent.getContext().getResources().getDimension(
+                    R.dimen.car_ui_recyclerview_divider_bottom_margin);
+
+            mHorizontalDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom);
+            mHorizontalDivider.draw(canvas);
+        }
+    }
+
+    public void setNumOfColumns(int numberOfColumns) {
+        mNumColumns = numberOfColumns;
+    }
+
+    /**
+     * Adds vertical dividers to a RecyclerView with a GridLayoutManager or its subclass.
+     *
+     * @param canvas The {@link Canvas} onto which dividers will be drawn
+     * @param parent The RecyclerView onto which dividers are being added
+     */
+    private void drawVerticalDividers(Canvas canvas, RecyclerView parent) {
+        double childCount = parent.getChildCount();
+        double rowCount = Math.ceil(childCount / mNumColumns);
+        int rightmostChildIndex;
+        for (int i = 1; i <= rowCount; i++) {
+            // we dont want the divider on top of first row.
+            if (i == 1) {
+                continue;
+            }
+            if (i == rowCount) {
+                rightmostChildIndex = ((i - 1) * mNumColumns) - 1;
+            } else {
+                rightmostChildIndex = (i * mNumColumns) - 1;
+            }
+
+            View leftmostChild = parent.getChildAt(mNumColumns * (i - 1));
+            View rightmostChild = parent.getChildAt(rightmostChildIndex);
+
+            // draws on top of each row.
+            int dividerLeft =
+                    leftmostChild.getLeft() + (int) parent.getContext().getResources().getDimension(
+                            R.dimen.car_ui_recyclerview_divider_start_margin);
+            int dividerBottom = leftmostChild.getTop();
+            int dividerTop = dividerBottom - mVerticalDivider.getIntrinsicHeight();
+            int dividerRight = rightmostChild.getRight()
+                    - (int) parent.getContext().getResources().getDimension(
+                    R.dimen.car_ui_recyclerview_divider_end_margin);
+
+            mVerticalDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom);
+            mVerticalDivider.draw(canvas);
+        }
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/grid/GridOffsetItemDecoration.java b/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/grid/GridOffsetItemDecoration.java
new file mode 100644
index 0000000..3dd6c19
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/grid/GridOffsetItemDecoration.java
@@ -0,0 +1,166 @@
+/*
+ * 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.recyclerview.decorations.grid;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.annotation.Retention;
+
+/** Adds an offset to the top of a RecyclerView with a GridLayoutManager or its subclass. */
+public class GridOffsetItemDecoration extends RecyclerView.ItemDecoration {
+
+    private int mOffsetPx;
+    private Drawable mOffsetDrawable;
+    private int mNumColumns;
+    @OffsetPosition
+    private final int mOffsetPosition;
+
+    /** The possible values for setScrollbarPosition. */
+    @IntDef({
+            OffsetPosition.START,
+            OffsetPosition.END,
+    })
+    @Retention(SOURCE)
+    public @interface OffsetPosition {
+        /** Position the offset to the start of the screen. */
+        int START = 0;
+
+        /** Position offset to the end of the screen. */
+        int END = 1;
+    }
+
+    /**
+     * Constructor that takes in the size of the offset to be added to the top of the RecyclerView.
+     *
+     * @param offsetPx The size of the offset to be added to the top of the RecyclerView in pixels
+     * @param numColumns The number of columns in the grid of the RecyclerView
+     * @param offsetPosition Position where offset needs to be applied.
+     */
+    public GridOffsetItemDecoration(int offsetPx, int numColumns, int offsetPosition) {
+        this.mOffsetPx = offsetPx;
+        this.mNumColumns = numColumns;
+        this.mOffsetPosition = offsetPosition;
+    }
+
+    /**
+     * Constructor that takes in a {@link Drawable} to be drawn at the top of the RecyclerView.
+     *
+     * @param offsetDrawable The {@code Drawable} to be added to the top of the RecyclerView
+     * @param numColumns The number of columns in the grid of the RecyclerView
+     */
+    public GridOffsetItemDecoration(Drawable offsetDrawable, int numColumns, int offsetPosition) {
+        this.mOffsetDrawable = offsetDrawable;
+        this.mNumColumns = numColumns;
+        this.mOffsetPosition = offsetPosition;
+    }
+
+    public void setNumOfColumns(int numberOfColumns) {
+        mNumColumns = numberOfColumns;
+    }
+
+    /**
+     * Determines the size and the location of the offset to be added to the top of the
+     * RecyclerView.
+     *
+     * @param outRect The {@link Rect} of offsets to be added around the child view
+     * @param view The child view to be decorated with an offset
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void getItemOffsets(
+            Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        super.getItemOffsets(outRect, view, parent, state);
+
+        if (mOffsetPosition == OffsetPosition.START) {
+            boolean childIsInTopRow = parent.getChildAdapterPosition(view) < mNumColumns;
+            if (childIsInTopRow) {
+                if (mOffsetPx > 0) {
+                    outRect.top = mOffsetPx;
+                } else if (mOffsetDrawable != null) {
+                    outRect.top = mOffsetDrawable.getIntrinsicHeight();
+                }
+            }
+            return;
+        }
+
+        int childCount = state.getItemCount();
+        int lastRowChildCount = getLastRowChildCount(childCount);
+
+        boolean childIsInBottomRow =
+                parent.getChildAdapterPosition(view) >= childCount - lastRowChildCount;
+        if (childIsInBottomRow) {
+            if (mOffsetPx > 0) {
+                outRect.bottom = mOffsetPx;
+            } else if (mOffsetDrawable != null) {
+                outRect.bottom = mOffsetDrawable.getIntrinsicHeight();
+            }
+        }
+    }
+
+    @Override
+    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        super.onDraw(c, parent, state);
+        if (mOffsetDrawable == null) {
+            return;
+        }
+
+        int parentLeft = parent.getPaddingLeft();
+        int parentRight = parent.getWidth() - parent.getPaddingRight();
+
+        if (mOffsetPosition == OffsetPosition.START) {
+
+            int parentTop = parent.getPaddingTop();
+            int offsetDrawableBottom = parentTop + mOffsetDrawable.getIntrinsicHeight();
+
+            mOffsetDrawable.setBounds(parentLeft, parentTop, parentRight, offsetDrawableBottom);
+            mOffsetDrawable.draw(c);
+            return;
+        }
+
+        int childCount = state.getItemCount();
+        int lastRowChildCount = getLastRowChildCount(childCount);
+
+        int offsetDrawableTop = 0;
+        int offsetDrawableBottom = 0;
+
+        for (int i = childCount - lastRowChildCount; i < childCount; i++) {
+            View child = parent.getChildAt(i);
+            offsetDrawableTop = child.getBottom();
+            offsetDrawableBottom = offsetDrawableTop + mOffsetDrawable.getIntrinsicHeight();
+        }
+
+        mOffsetDrawable.setBounds(parentLeft, offsetDrawableTop, parentRight, offsetDrawableBottom);
+        mOffsetDrawable.draw(c);
+    }
+
+    private int getLastRowChildCount(int itemCount) {
+        int lastRowChildCount = itemCount % mNumColumns;
+        if (lastRowChildCount == 0) {
+            lastRowChildCount = mNumColumns;
+        }
+
+        return lastRowChildCount;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/linear/LinearDividerItemDecoration.java b/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/linear/LinearDividerItemDecoration.java
new file mode 100644
index 0000000..0adbdeb
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/linear/LinearDividerItemDecoration.java
@@ -0,0 +1,143 @@
+/*
+ * 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.recyclerview.decorations.linear;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+
+/** Adds interior dividers to a RecyclerView with a LinearLayoutManager or its subclass. */
+public class LinearDividerItemDecoration extends RecyclerView.ItemDecoration {
+
+    private final Drawable mDivider;
+    private int mOrientation;
+
+    /**
+     * Sole constructor. Takes in a {@link Drawable} to be used as the interior
+     * car_ui_recyclerview_divider.
+     *
+     * @param divider A car_ui_recyclerview_divider {@code Drawable} to be drawn on the
+     * RecyclerView
+     */
+    public LinearDividerItemDecoration(Drawable divider) {
+        this.mDivider = divider;
+    }
+
+    /**
+     * Draws horizontal or vertical dividers onto the parent RecyclerView.
+     *
+     * @param canvas The {@link Canvas} onto which dividers will be drawn
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
+        if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+            drawHorizontalDividers(canvas, parent);
+        } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+            drawVerticalDividers(canvas, parent);
+        }
+    }
+
+    /**
+     * Determines the size and location of offsets between items in the parent RecyclerView.
+     *
+     * @param outRect The {@link Rect} of offsets to be added around the child view
+     * @param view The child view to be decorated with an offset
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void getItemOffsets(
+            Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        super.getItemOffsets(outRect, view, parent, state);
+
+        if (parent.getChildAdapterPosition(view) == 0) {
+            return;
+        }
+
+        mOrientation = ((LinearLayoutManager) parent.getLayoutManager()).getOrientation();
+        if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+            outRect.left = mDivider.getIntrinsicWidth();
+        } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+            outRect.top = mDivider.getIntrinsicHeight();
+        }
+    }
+
+    /**
+     * Adds dividers to a RecyclerView with a LinearLayoutManager or its subclass oriented
+     * horizontally.
+     *
+     * @param canvas The {@link Canvas} onto which horizontal dividers will be drawn
+     * @param parent The RecyclerView onto which horizontal dividers are being added
+     */
+    private void drawHorizontalDividers(Canvas canvas, RecyclerView parent) {
+        int parentTop =
+                parent.getPaddingTop() + (int) parent.getContext().getResources().getDimension(
+                        R.dimen.car_ui_recyclerview_divider_top_margin);
+        int parentBottom = parent.getHeight() - parent.getPaddingBottom()
+                - (int) parent.getContext().getResources().getDimension(
+                R.dimen.car_ui_recyclerview_divider_bottom_margin);
+
+        int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount - 1; i++) {
+            View child = parent.getChildAt(i);
+
+            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
+
+            int parentLeft = child.getRight() + params.rightMargin;
+            int parentRight = parentLeft + mDivider.getIntrinsicWidth();
+
+            mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom);
+            mDivider.draw(canvas);
+        }
+    }
+
+    /**
+     * Adds dividers to a RecyclerView with a LinearLayoutManager or its subclass oriented
+     * vertically.
+     *
+     * @param canvas The {@link Canvas} onto which vertical dividers will be drawn
+     * @param parent The RecyclerView onto which vertical dividers are being added
+     */
+    private void drawVerticalDividers(Canvas canvas, RecyclerView parent) {
+        int parentLeft =
+                parent.getPaddingLeft() + (int) parent.getContext().getResources().getDimension(
+                        R.dimen.car_ui_recyclerview_divider_start_margin);
+        int parentRight = parent.getWidth() - parent.getPaddingRight()
+                - (int) parent.getContext().getResources().getDimension(
+                R.dimen.car_ui_recyclerview_divider_end_margin);
+
+        int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount - 1; i++) {
+            View child = parent.getChildAt(i);
+
+            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
+
+            int parentTop = child.getBottom() + params.bottomMargin;
+            int parentBottom = parentTop + mDivider.getIntrinsicHeight();
+
+            mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom);
+            mDivider.draw(canvas);
+        }
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/linear/LinearOffsetItemDecoration.java b/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/linear/LinearOffsetItemDecoration.java
new file mode 100644
index 0000000..d757f05
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/decorations/linear/LinearOffsetItemDecoration.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 com.android.car.ui.recyclerview.decorations.linear;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Adds an offset to the start of a RecyclerView using a LinearLayoutManager or its subclass.
+ *
+ * <p>If the RecyclerView.LayoutManager is oriented vertically, the offset will be added to the top
+ * of the RecyclerView. If the LayoutManager is oriented horizontally, the offset will be added to
+ * the left of the RecyclerView.
+ */
+public class LinearOffsetItemDecoration extends RecyclerView.ItemDecoration {
+
+    private int mOffsetPx;
+    private Drawable mOffsetDrawable;
+    private int mOrientation;
+    @OffsetPosition
+    private int mOffsetPosition;
+
+    /** The possible values for setScrollbarPosition. */
+    @IntDef({
+            OffsetPosition.START,
+            OffsetPosition.END,
+    })
+    @Retention(SOURCE)
+    public @interface OffsetPosition {
+        /** Position the offset to the start of the screen. */
+        int START = 0;
+
+        /** Position offset to the end of the screen. */
+        int END = 1;
+    }
+
+    /**
+     * Constructor that takes in the size of the offset to be added to the start of the
+     * RecyclerView.
+     *
+     * @param offsetPx The size of the offset to be added to the start of the RecyclerView in pixels
+     * @param offsetPosition Position where offset needs to be applied.
+     */
+    public LinearOffsetItemDecoration(int offsetPx, int offsetPosition) {
+        this.mOffsetPx = offsetPx;
+        this.mOffsetPosition = offsetPosition;
+    }
+
+    /**
+     * Constructor that takes in a {@link Drawable} to be drawn at the start of the RecyclerView.
+     *
+     * @param offsetDrawable The {@code Drawable} to be added to the start of the RecyclerView
+     */
+    public LinearOffsetItemDecoration(Drawable offsetDrawable) {
+        this.mOffsetDrawable = offsetDrawable;
+    }
+
+    /**
+     * Determines the size and location of the offset to be added to the start of the RecyclerView.
+     *
+     * @param outRect The {@link Rect} of offsets to be added around the child view
+     * @param view The child view to be decorated with an offset
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void getItemOffsets(
+            Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        super.getItemOffsets(outRect, view, parent, state);
+
+        if (mOffsetPosition == OffsetPosition.START && parent.getChildAdapterPosition(view) > 0) {
+            return;
+        }
+
+        int itemCount = state.getItemCount();
+        if (mOffsetPosition == OffsetPosition.END
+                && parent.getChildAdapterPosition(view) != itemCount - 1) {
+            return;
+        }
+
+        mOrientation = ((LinearLayoutManager) parent.getLayoutManager()).getOrientation();
+        if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+            if (mOffsetPx > 0) {
+                if (mOffsetPosition == OffsetPosition.START) {
+                    outRect.left = mOffsetPx;
+                } else {
+                    outRect.right = mOffsetPx;
+                }
+            } else if (mOffsetDrawable != null) {
+                if (mOffsetPosition == OffsetPosition.START) {
+                    outRect.left = mOffsetDrawable.getIntrinsicWidth();
+                } else {
+                    outRect.right = mOffsetDrawable.getIntrinsicWidth();
+                }
+            }
+        } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+            if (mOffsetPx > 0) {
+                if (mOffsetPosition == OffsetPosition.START) {
+                    outRect.top = mOffsetPx;
+                } else {
+                    outRect.bottom = mOffsetPx;
+                }
+            } else if (mOffsetDrawable != null) {
+                if (mOffsetPosition == OffsetPosition.START) {
+                    outRect.top = mOffsetDrawable.getIntrinsicHeight();
+                } else {
+                    outRect.bottom = mOffsetDrawable.getIntrinsicHeight();
+                }
+            }
+        }
+    }
+
+    /**
+     * Draws horizontal or vertical offset onto the start of the parent RecyclerView.
+     *
+     * @param c The {@link Canvas} onto which an offset will be drawn
+     * @param parent The RecyclerView onto which an offset is being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        super.onDraw(c, parent, state);
+        if (mOffsetDrawable == null) {
+            return;
+        }
+
+        if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+            drawOffsetHorizontal(c, parent);
+        } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+            drawOffsetVertical(c, parent);
+        }
+    }
+
+    private void drawOffsetHorizontal(Canvas canvas, RecyclerView parent) {
+        int parentTop = parent.getPaddingTop();
+        int parentBottom = parent.getHeight() - parent.getPaddingBottom();
+        int parentLeft = 0;
+        int offsetDrawableRight = 0;
+
+        if (mOffsetPosition == OffsetPosition.START) {
+            parentLeft = parent.getPaddingLeft();
+            offsetDrawableRight = parentLeft + mOffsetDrawable.getIntrinsicWidth();
+        } else {
+            View lastChild = parent.getChildAt(parent.getChildCount() - 1);
+            RecyclerView.LayoutParams lastChildLayoutParams =
+                    (RecyclerView.LayoutParams) lastChild.getLayoutParams();
+            parentLeft = lastChild.getRight() + lastChildLayoutParams.rightMargin;
+            offsetDrawableRight = parentLeft + mOffsetDrawable.getIntrinsicWidth();
+        }
+
+        mOffsetDrawable.setBounds(parentLeft, parentTop, offsetDrawableRight, parentBottom);
+        mOffsetDrawable.draw(canvas);
+    }
+
+    private void drawOffsetVertical(Canvas canvas, RecyclerView parent) {
+        int parentLeft = parent.getPaddingLeft();
+        int parentRight = parent.getWidth() - parent.getPaddingRight();
+
+        int parentTop = 0;
+        int offsetDrawableBottom = 0;
+
+        if (mOffsetPosition == OffsetPosition.START) {
+            parentTop = parent.getPaddingTop();
+            offsetDrawableBottom = parentTop + mOffsetDrawable.getIntrinsicHeight();
+        } else {
+            View lastChild = parent.getChildAt(parent.getChildCount() - 1);
+            RecyclerView.LayoutParams lastChildLayoutParams =
+                    (RecyclerView.LayoutParams) lastChild.getLayoutParams();
+            parentTop = lastChild.getBottom() + lastChildLayoutParams.bottomMargin;
+            offsetDrawableBottom = parentTop + mOffsetDrawable.getIntrinsicHeight();
+        }
+
+        mOffsetDrawable.setBounds(parentLeft, parentTop, parentRight, offsetDrawableBottom);
+        mOffsetDrawable.draw(canvas);
+    }
+}
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
new file mode 100644
index 0000000..20d6847
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
@@ -0,0 +1,613 @@
+/*
+ * 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.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.ui.R;
+import com.android.car.ui.utils.CarUxRestrictionsUtil;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Represents a button to display in the {@link Toolbar}.
+ *
+ * <p>There are currently 3 types of buttons: icon, text, and switch. Using
+ * {@link Builder#setCheckable()} will ensure that you get a switch, after that
+ * {@link Builder#setIcon(int)} will ensure an icon, and anything left just requires
+ * {@link Builder#setTitle(int)}.
+ *
+ * <p>Each MenuItem has a {@link DisplayBehavior} that controls if it appears on the {@link Toolbar}
+ * itself, or it's overflow menu.
+ *
+ * <p>If you require a search or settings button, you should use
+ * {@link Builder#setToSearch()} or
+ * {@link Builder#setToSettings()}.
+ *
+ * <p>Some properties can be changed after the creating a MenuItem, but others require being set
+ * with a {@link Builder}.
+ */
+public class MenuItem {
+
+    private final Context mContext;
+    private final boolean mIsCheckable;
+    private final boolean mIsActivatable;
+    private final boolean mIsSearch;
+    private final boolean mShowIconAndTitle;
+    private final boolean mIsTinted;
+    @CarUxRestrictions.CarUxRestrictionsInfo
+
+    private int mId;
+    private CarUxRestrictions mCurrentRestrictions;
+    // This is a WeakReference to allow the Toolbar (and by extension, the whole screen
+    // the toolbar is on) to be garbage-collected if the MenuItem is held past the
+    // lifecycle of the toolbar.
+    private WeakReference<Listener> mListener = new WeakReference<>(null);
+    private CharSequence mTitle;
+    private Drawable mIcon;
+    private OnClickListener mOnClickListener;
+    private DisplayBehavior mDisplayBehavior;
+    private int mUxRestrictions;
+    private boolean mIsEnabled;
+    private boolean mIsChecked;
+    private boolean mIsVisible;
+    private boolean mIsActivated;
+
+    private MenuItem(Builder builder) {
+        mContext = builder.mContext;
+        mId = builder.mId;
+        mIsCheckable = builder.mIsCheckable;
+        mIsActivatable = builder.mIsActivatable;
+        mTitle = builder.mTitle;
+        mIcon = builder.mIcon;
+        mOnClickListener = builder.mOnClickListener;
+        mDisplayBehavior = builder.mDisplayBehavior;
+        mIsEnabled = builder.mIsEnabled;
+        mIsChecked = builder.mIsChecked;
+        mIsVisible = builder.mIsVisible;
+        mIsActivated = builder.mIsActivated;
+        mIsSearch = builder.mIsSearch;
+        mShowIconAndTitle = builder.mShowIconAndTitle;
+        mIsTinted = builder.mIsTinted;
+        mUxRestrictions = builder.mUxRestrictions;
+
+        mCurrentRestrictions = CarUxRestrictionsUtil.getInstance(mContext).getCurrentRestrictions();
+    }
+
+    private void update() {
+        Listener listener = mListener.get();
+        if (listener != null) {
+            listener.onMenuItemChanged();
+        }
+    }
+
+    /** Sets the id, which is purely for the client to distinguish MenuItems with.  */
+    public void setId(int id) {
+        mId = id;
+        update();
+    }
+
+    /** Gets the id, which is purely for the client to distinguish MenuItems with. */
+    public int getId() {
+        return mId;
+    }
+
+    /** Returns whether the MenuItem is enabled */
+    public boolean isEnabled() {
+        return mIsEnabled;
+    }
+
+    /** Sets whether the MenuItem is enabled */
+    public void setEnabled(boolean enabled) {
+        mIsEnabled = enabled;
+
+        update();
+    }
+
+    /** Returns whether the MenuItem is checkable. If it is, it will be displayed as a switch. */
+    public boolean isCheckable() {
+        return mIsCheckable;
+    }
+
+    /**
+     * Returns whether the MenuItem is currently checked. Only valid if {@link #isCheckable()}
+     * is true.
+     */
+    public boolean isChecked() {
+        return mIsChecked;
+    }
+
+    /**
+     * Sets whether or not the MenuItem is checked.
+     * @throws IllegalStateException When {@link #isCheckable()} is false.
+     */
+    public void setChecked(boolean checked) {
+        if (!isCheckable()) {
+            throw new IllegalStateException("Cannot call setChecked() on a non-checkable MenuItem");
+        }
+
+        mIsChecked = checked;
+
+        update();
+    }
+
+    public boolean isTinted() {
+        return mIsTinted;
+    }
+
+    /** Returns whether or not the MenuItem is visible */
+    public boolean isVisible() {
+        return mIsVisible;
+    }
+
+    /** Sets whether or not the MenuItem is visible */
+    public void setVisible(boolean visible) {
+        mIsVisible = visible;
+
+        update();
+    }
+
+    /**
+     * Returns whether the MenuItem is activatable. If it is, it's every click will toggle
+     * the MenuItem's View to appear activated or not.
+     */
+    public boolean isActivatable() {
+        return mIsActivatable;
+    }
+
+    /** Returns whether or not this view is selected. Toggles after every click */
+    public boolean isActivated() {
+        return mIsActivated;
+    }
+
+    /** Sets the MenuItem as activated and updates it's View to the activated state */
+    public void setActivated(boolean activated) {
+        if (!isActivatable()) {
+            throw new IllegalStateException(
+                    "Cannot call setActivated() on a non-activatable MenuItem");
+        }
+
+        mIsActivated = activated;
+
+        update();
+    }
+
+    /** Gets the title of this MenuItem. */
+    public CharSequence getTitle() {
+        return mTitle;
+    }
+
+    /** Sets the title of this MenuItem. */
+    public void setTitle(CharSequence title) {
+        mTitle = title;
+
+        update();
+    }
+
+    /** Sets the title of this MenuItem to a string resource. */
+    public void setTitle(int resId) {
+        setTitle(mContext.getString(resId));
+    }
+
+    /** Sets the UxRestrictions of this MenuItem. */
+    public void setUxRestrictions(@CarUxRestrictions.CarUxRestrictionsInfo int uxRestrictions) {
+        if (mUxRestrictions != uxRestrictions) {
+            mUxRestrictions = uxRestrictions;
+            update();
+        }
+    }
+
+    @CarUxRestrictions.CarUxRestrictionsInfo
+    public int getUxRestrictions() {
+        return mUxRestrictions;
+    }
+
+    /** Gets the current {@link OnClickListener} */
+    public OnClickListener getOnClickListener() {
+        return mOnClickListener;
+    }
+
+    public boolean isShowingIconAndTitle() {
+        return mShowIconAndTitle;
+    }
+
+    /** Sets the {@link OnClickListener} */
+    public void setOnClickListener(OnClickListener listener) {
+        mOnClickListener = listener;
+
+        update();
+    }
+
+    /* package */ void setCarUxRestrictions(CarUxRestrictions restrictions) {
+        boolean wasRestricted = isRestricted();
+        mCurrentRestrictions = restrictions;
+
+        if (isRestricted() != wasRestricted) {
+            update();
+        }
+    }
+
+    /* package */ boolean isRestricted() {
+        return CarUxRestrictionsUtil.isRestricted(mUxRestrictions, mCurrentRestrictions);
+    }
+
+    /** Calls the {@link OnClickListener}. */
+    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+    public void performClick() {
+        if (!isEnabled() || !isVisible()) {
+            return;
+        }
+
+        if (isRestricted()) {
+            Toast.makeText(mContext,
+                    R.string.car_ui_restricted_while_driving, Toast.LENGTH_LONG).show();
+            return;
+        }
+
+        if (isActivatable()) {
+            setActivated(!isActivated());
+        }
+
+        if (isCheckable()) {
+            setChecked(!isChecked());
+        }
+
+        if (mOnClickListener != null) {
+            mOnClickListener.onClick(this);
+        }
+    }
+
+    /** Gets the current {@link DisplayBehavior} */
+    public DisplayBehavior getDisplayBehavior() {
+        return mDisplayBehavior;
+    }
+
+    /** Gets the current Icon */
+    public Drawable getIcon() {
+        return mIcon;
+    }
+
+    /** Sets the Icon of this MenuItem. */
+    public void setIcon(Drawable icon) {
+        mIcon = icon;
+
+        update();
+    }
+
+    /** Sets the Icon of this MenuItem to a drawable resource. */
+    public void setIcon(int resId) {
+        setIcon(resId == 0
+                ? null
+                : mContext.getDrawable(resId));
+    }
+
+    /** Returns if this is the search MenuItem, which has special behavior when searching */
+    boolean isSearch() {
+        return mIsSearch;
+    }
+
+    /** Builder class */
+    public static final class Builder {
+        private final Context mContext;
+
+        private String mSearchTitle;
+        private String mSettingsTitle;
+        private Drawable mSearchIcon;
+        private Drawable mSettingsIcon;
+
+        private int mId = View.NO_ID;
+        private CharSequence mTitle;
+        private Drawable mIcon;
+        private OnClickListener mOnClickListener;
+        private DisplayBehavior mDisplayBehavior = DisplayBehavior.ALWAYS;
+        private boolean mIsTinted = true;
+        private boolean mShowIconAndTitle = false;
+        private boolean mIsEnabled = true;
+        private boolean mIsCheckable = false;
+        private boolean mIsChecked = false;
+        private boolean mIsVisible = true;
+        private boolean mIsActivatable = false;
+        private boolean mIsActivated = false;
+        private boolean mIsSearch = false;
+        private boolean mIsSettings = false;
+        @CarUxRestrictions.CarUxRestrictionsInfo
+        private int mUxRestrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
+
+        public Builder(Context c) {
+            // Must use getApplicationContext to avoid leaking activities when the MenuItem
+            // is held onto for longer than the Activity's lifecycle
+            mContext = c.getApplicationContext();
+        }
+
+        /** Builds a {@link MenuItem} from the current state of the Builder */
+        public MenuItem build() {
+            if (mIsActivatable && (mShowIconAndTitle || mIcon == null)) {
+                throw new IllegalStateException("Only simple icons can be activatable");
+            }
+            if (mIsCheckable
+                    && (mDisplayBehavior == DisplayBehavior.NEVER
+                    || mShowIconAndTitle
+                    || mIsActivatable)) {
+                throw new IllegalStateException("Unsupported options for a checkable MenuItem");
+            }
+            if (mIsSearch && mIsSettings) {
+                throw new IllegalStateException("Can't have both a search and settings MenuItem");
+            }
+
+            if (mIsSearch && (!mSearchTitle.contentEquals(mTitle)
+                    || !mSearchIcon.equals(mIcon)
+                    || mIsCheckable
+                    || mIsActivatable
+                    || !mIsTinted
+                    || mShowIconAndTitle
+                    || mDisplayBehavior != DisplayBehavior.ALWAYS)) {
+                throw new IllegalStateException("Invalid search MenuItem");
+            }
+
+            if (mIsSettings && (!mSettingsTitle.contentEquals(mTitle)
+                    || !mSettingsIcon.equals(mIcon)
+                    || mIsCheckable
+                    || mIsActivatable
+                    || !mIsTinted
+                    || mShowIconAndTitle
+                    || mDisplayBehavior != DisplayBehavior.ALWAYS)) {
+                throw new IllegalStateException("Invalid settings MenuItem");
+            }
+
+            return new MenuItem(this);
+        }
+
+        /** Sets the id, which is purely for the client to distinguish MenuItems with. */
+        public Builder setId(int id) {
+            mId = id;
+            return this;
+        }
+
+        /** Sets the title to a string resource id */
+        public Builder setTitle(int resId) {
+            setTitle(mContext.getString(resId));
+            return this;
+        }
+
+        /** Sets the title */
+        public Builder setTitle(CharSequence title) {
+            mTitle = title;
+            return this;
+        }
+
+        /**
+         * Sets the icon to a drawable resource id.
+         *
+         * <p>The icon's color and size will be changed to match the other MenuItems.
+         */
+        public Builder setIcon(int resId) {
+            mIcon = resId == 0
+                    ? null
+                    : mContext.getDrawable(resId);
+            return this;
+        }
+
+        /**
+         * Sets the icon to a drawable.
+         *
+         * <p>The icon's color and size will be changed to match the other MenuItems.
+         */
+        public Builder setIcon(Drawable icon) {
+            mIcon = icon;
+            return this;
+        }
+
+        /**
+         * Sets whether to tint the icon, true by default.
+         *
+         * <p>Try not to use this, it should only be used if the MenuItem is displaying some
+         * kind of logo or avatar and should be colored.
+         */
+        public Builder setTinted(boolean tinted) {
+            mIsTinted = tinted;
+            return this;
+        }
+
+        /** Sets whether the MenuItem is visible or not. Default true. */
+        public Builder setVisible(boolean visible) {
+            mIsVisible = visible;
+            return this;
+        }
+
+        /**
+         * Makes the MenuItem activatable, which means it will toggle it's visual state after
+         * every click.
+         */
+        public Builder setActivatable() {
+            mIsActivatable = true;
+            return this;
+        }
+
+        /**
+         * Sets whether or not the MenuItem is selected. If it is,
+         * {@link View#setSelected(boolean)} will be called on its View.
+         */
+        public Builder setActivated(boolean activated) {
+            setActivatable();
+            mIsActivated = activated;
+            return this;
+        }
+
+        /** Sets the {@link OnClickListener} */
+        public Builder setOnClickListener(OnClickListener listener) {
+            mOnClickListener = listener;
+            return this;
+        }
+
+        /**
+         * Used to show both the icon and title when displayed on the toolbar. If this
+         * is false, only the icon while be displayed when the MenuItem is in the toolbar
+         * and only the title will be displayed when the MenuItem is in the overflow menu.
+         *
+         * <p>Defaults to false.
+         */
+        public Builder setShowIconAndTitle(boolean showIconAndTitle) {
+            mShowIconAndTitle = showIconAndTitle;
+            return this;
+        }
+
+        /**
+         * Sets the {@link DisplayBehavior}.
+         *
+         * <p>If the DisplayBehavior is {@link DisplayBehavior#NEVER}, the MenuItem must not be
+         * {@link #setCheckable() checkable}.
+         */
+        public Builder setDisplayBehavior(DisplayBehavior behavior) {
+            mDisplayBehavior = behavior;
+            return this;
+        }
+
+        /** Sets whether the MenuItem is enabled or not. Default true. */
+        public Builder setEnabled(boolean enabled) {
+            mIsEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Makes the MenuItem checkable, meaning it will be displayed as a
+         * switch. Currently a checkable MenuItem cannot have a {@link DisplayBehavior} of NEVER.
+         *
+         * <p>The MenuItem is not checkable by default.
+         */
+        public Builder setCheckable() {
+            mIsCheckable = true;
+            return this;
+        }
+
+        /**
+         * Sets whether the MenuItem is checked or not. This will imply {@link #setCheckable()}.
+         */
+        public Builder setChecked(boolean checked) {
+            setCheckable();
+            mIsChecked = checked;
+            return this;
+        }
+
+        /**
+         * Sets under what {@link android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo}
+         * the MenuItem should be restricted.
+         */
+        public Builder setUxRestrictions(
+                @CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
+            mUxRestrictions = restrictions;
+            return this;
+        }
+
+        /**
+         * Creates a search MenuItem.
+         *
+         * <p>The advantage of using this over creating your own is getting an OEM-styled search
+         * icon, and this button will always disappear while searching, even when the
+         * {@link Toolbar Toolbar's} showMenuItemsWhileSearching is true.
+         *
+         * <p>If using this, you should only change the id, visibility, or onClickListener.
+         */
+        public Builder setToSearch() {
+            mSearchTitle = mContext.getString(R.string.car_ui_toolbar_menu_item_search_title);
+            mSearchIcon = mContext.getDrawable(R.drawable.car_ui_icon_search);
+            mIsSearch = true;
+            setTitle(mSearchTitle);
+            setIcon(mSearchIcon);
+            return this;
+        }
+
+        /**
+         * Creates a settings MenuItem.
+         *
+         * <p>The advantage of this over creating your own is getting an OEM-styled settings icon,
+         * and that the MenuItem will be restricted based on
+         * {@link CarUxRestrictions#UX_RESTRICTIONS_NO_SETUP}
+         *
+         * <p>If using this, you should only change the id, visibility, or onClickListener.
+         */
+        public Builder setToSettings() {
+            mSettingsTitle = mContext.getString(R.string.car_ui_toolbar_menu_item_settings_title);
+            mSettingsIcon = mContext.getDrawable(R.drawable.car_ui_icon_settings);
+            mIsSettings = true;
+            setTitle(mSettingsTitle);
+            setIcon(mSettingsIcon);
+            setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP);
+            return this;
+        }
+
+        /** @deprecated Use {@link #setToSearch()} instead. */
+        @Deprecated
+        public static MenuItem createSearch(Context c, OnClickListener listener) {
+            return MenuItem.builder(c)
+                    .setToSearch()
+                    .setOnClickListener(listener)
+                    .build();
+        }
+
+        /** @deprecated Use {@link #setToSettings()} instead. */
+        @Deprecated
+        public static MenuItem createSettings(Context c, OnClickListener listener) {
+            return MenuItem.builder(c)
+                    .setToSettings()
+                    .setOnClickListener(listener)
+                    .build();
+        }
+    }
+
+    /** Get a new {@link Builder}. */
+    public static Builder builder(Context context) {
+        return new Builder(context);
+    }
+
+    /**
+     * OnClickListener for a MenuItem.
+     */
+    public interface OnClickListener {
+        /** Called when the MenuItem is clicked */
+        void onClick(MenuItem item);
+    }
+
+    /**
+     * DisplayBehavior controls how the MenuItem is presented in the Toolbar
+     */
+    public enum DisplayBehavior {
+        /** Always show the MenuItem on the toolbar instead of the overflow menu */
+        ALWAYS,
+        /** Never show the MenuItem in the toolbar, always put it in the overflow menu */
+        NEVER
+    }
+
+    /** Listener for {@link Toolbar} to update when this MenuItem changes */
+    interface Listener {
+        /** Called when the MenuItem is changed. For use only by {@link Toolbar} */
+        void onMenuItemChanged();
+    }
+
+    /**
+     * Sets a listener for changes to this MenuItem. Note that the MenuItem will only hold
+     * weak references to the Listener, so that the listener is not held if the MenuItem
+     * outlives the toolbar.
+     */
+    void setListener(Listener listener) {
+        mListener = new WeakReference<>(listener);
+    }
+}
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
new file mode 100644
index 0000000..77b9f53
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
@@ -0,0 +1,298 @@
+/*
+ * 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 static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.app.Activity;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.Switch;
+import android.widget.TextView;
+
+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;
+import com.android.car.ui.uxr.DrawableStateView;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+class MenuItemRenderer implements MenuItem.Listener {
+
+    private static final int[] RESTRICTED_STATE = new int[] {R.attr.state_ux_restricted};
+
+    private final int mMenuItemIconSize;
+
+    private Toolbar.State mToolbarState;
+
+    private final MenuItem mMenuItem;
+    private final ViewGroup mParentView;
+    private View mView;
+    private View mIconContainer;
+    private ImageView mIconView;
+    private Switch mSwitch;
+    private TextView mTextView;
+    private TextView mTextWithIconView;
+
+    MenuItemRenderer(MenuItem item, ViewGroup parentView) {
+        mMenuItem = item;
+        mParentView = parentView;
+        mMenuItem.setListener(this);
+
+        mMenuItemIconSize = parentView.getContext().getResources()
+                .getDimensionPixelSize(R.dimen.car_ui_toolbar_menu_item_icon_size);
+    }
+
+    void setToolbarState(Toolbar.State state) {
+        mToolbarState = state;
+
+        if (mMenuItem.isSearch()) {
+            updateView();
+        }
+    }
+
+    void setCarUxRestrictions(CarUxRestrictions restrictions) {
+        mMenuItem.setCarUxRestrictions(restrictions);
+    }
+
+    @Override
+    public void onMenuItemChanged() {
+        updateView();
+    }
+
+    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 =
+                    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() {
+        if (mView == null) {
+            return;
+        }
+
+        mView.setId(mMenuItem.getId());
+
+        boolean hasIcon = mMenuItem.getIcon() != null;
+        boolean hasText = !TextUtils.isEmpty(mMenuItem.getTitle());
+        boolean textAndIcon = mMenuItem.isShowingIconAndTitle();
+        boolean checkable = mMenuItem.isCheckable();
+
+        if (!mMenuItem.isVisible()
+                || (mMenuItem.isSearch() && mToolbarState == Toolbar.State.SEARCH)
+                || (!checkable && !hasIcon && !hasText)) {
+            mView.setVisibility(View.GONE);
+            return;
+        }
+        mView.setVisibility(View.VISIBLE);
+        mView.setContentDescription(mMenuItem.getTitle());
+
+        mIconContainer.setVisibility(View.GONE);
+        mTextView.setVisibility(View.GONE);
+        mTextWithIconView.setVisibility(View.GONE);
+        mSwitch.setVisibility(View.GONE);
+        if (checkable) {
+            mSwitch.setChecked(mMenuItem.isChecked());
+            mSwitch.setVisibility(View.VISIBLE);
+        } else if (hasText && hasIcon && textAndIcon) {
+            mMenuItem.getIcon().setBounds(0, 0, mMenuItemIconSize, mMenuItemIconSize);
+            mTextWithIconView.setCompoundDrawables(mMenuItem.getIcon(), null, null, null);
+            mTextWithIconView.setText(mMenuItem.getTitle());
+            mTextWithIconView.setVisibility(View.VISIBLE);
+        } else if (hasIcon) {
+            mIconView.setImageDrawable(mMenuItem.getIcon());
+            mIconContainer.setVisibility(View.VISIBLE);
+        } else { // hasText will be true
+            mTextView.setText(mMenuItem.getTitle());
+            mTextView.setVisibility(View.VISIBLE);
+        }
+
+        if (!mMenuItem.isTinted() && hasIcon) {
+            mMenuItem.getIcon().setTintList(null);
+        }
+
+        recursiveSetEnabledAndDrawableState(mView);
+        mView.setActivated(mMenuItem.isActivated());
+
+        if (mMenuItem.getOnClickListener() != null
+                || mMenuItem.isCheckable()
+                || mMenuItem.isActivatable()) {
+            mView.setOnClickListener(v -> mMenuItem.performClick());
+        } else {
+            mView.setOnClickListener(null);
+            mView.setClickable(false);
+        }
+    }
+
+    private void recursiveSetEnabledAndDrawableState(View view) {
+        view.setEnabled(mMenuItem.isEnabled());
+
+        int[] drawableState = mMenuItem.isRestricted() ? RESTRICTED_STATE : null;
+        if (view instanceof ImageView) {
+            ((ImageView) view).setImageState(drawableState, true);
+        } else if (view instanceof DrawableStateView) {
+            ((DrawableStateView) view).setDrawableState(drawableState);
+        }
+
+        if (view instanceof ViewGroup) {
+            ViewGroup viewGroup = ((ViewGroup) view);
+            for (int i = 0; i < viewGroup.getChildCount(); i++) {
+                recursiveSetEnabledAndDrawableState(viewGroup.getChildAt(i));
+            }
+        }
+    }
+
+    static List<MenuItem> readMenuItemList(Context c, @XmlRes int resId) {
+        if (resId == 0) {
+            return new ArrayList<>();
+        }
+
+        try (XmlResourceParser parser = c.getResources().getXml(resId)) {
+            AttributeSet attrs = Xml.asAttributeSet(parser);
+            List<MenuItem> menuItems = new ArrayList<>();
+
+            parser.next();
+            parser.next();
+            parser.require(XmlPullParser.START_TAG, null, "MenuItems");
+            while (parser.next() != XmlPullParser.END_TAG) {
+                menuItems.add(readMenuItem(c, parser, attrs));
+            }
+
+            return menuItems;
+        } catch (XmlPullParserException | IOException e) {
+            throw new RuntimeException("Unable to parse Menu Items", e);
+        }
+    }
+
+    private static MenuItem readMenuItem(Context c, XmlResourceParser parser, AttributeSet attrs)
+            throws XmlPullParserException, IOException {
+
+        parser.require(XmlPullParser.START_TAG, null, "MenuItem");
+
+        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CarUiToolbarMenuItem);
+        try {
+            int id = a.getResourceId(R.styleable.CarUiToolbarMenuItem_id, View.NO_ID);
+            String title = a.getString(R.styleable.CarUiToolbarMenuItem_title);
+            Drawable icon = a.getDrawable(R.styleable.CarUiToolbarMenuItem_icon);
+            boolean isSearch = a.getBoolean(R.styleable.CarUiToolbarMenuItem_search, false);
+            boolean isSettings = a.getBoolean(R.styleable.CarUiToolbarMenuItem_settings, false);
+            boolean tinted = a.getBoolean(R.styleable.CarUiToolbarMenuItem_tinted, true);
+            boolean visible = a.getBoolean(R.styleable.CarUiToolbarMenuItem_visible, true);
+            boolean showIconAndTitle = a.getBoolean(
+                    R.styleable.CarUiToolbarMenuItem_showIconAndTitle, false);
+            boolean checkable = a.getBoolean(R.styleable.CarUiToolbarMenuItem_checkable, false);
+            boolean checked = a.getBoolean(R.styleable.CarUiToolbarMenuItem_checked, false);
+            boolean checkedExists = a.hasValue(R.styleable.CarUiToolbarMenuItem_checked);
+            boolean activatable = a.getBoolean(R.styleable.CarUiToolbarMenuItem_activatable, false);
+            boolean activated = a.getBoolean(R.styleable.CarUiToolbarMenuItem_activated, false);
+            boolean activatedExists = a.hasValue(R.styleable.CarUiToolbarMenuItem_activated);
+            int displayBehaviorInt = a.getInt(R.styleable.CarUiToolbarMenuItem_displayBehavior, 0);
+            int uxRestrictions = a.getInt(R.styleable.CarUiToolbarMenuItem_uxRestrictions, 0);
+            String onClickMethod = a.getString(R.styleable.CarUiToolbarMenuItem_onClick);
+            MenuItem.OnClickListener onClickListener = null;
+
+            if (onClickMethod != null) {
+                Activity activity = CarUiUtils.getActivity(c);
+                if (activity == null) {
+                    throw new RuntimeException("Couldn't find an activity for the MenuItem");
+                }
+
+                try {
+                    Method m = activity.getClass().getMethod(onClickMethod, MenuItem.class);
+                    onClickListener = i -> {
+                        try {
+                            m.invoke(activity, i);
+                        } catch (InvocationTargetException | IllegalAccessException e) {
+                            throw new RuntimeException("Couldn't call the MenuItem's listener", e);
+                        }
+                    };
+                } catch (NoSuchMethodException e) {
+                    throw new RuntimeException("OnClick method "
+                            + onClickMethod + "(MenuItem) not found in your activity", e);
+                }
+            }
+
+            MenuItem.DisplayBehavior displayBehavior = displayBehaviorInt == 0
+                    ? MenuItem.DisplayBehavior.ALWAYS
+                    : MenuItem.DisplayBehavior.NEVER;
+
+            parser.next();
+            parser.require(XmlPullParser.END_TAG, null, "MenuItem");
+
+            MenuItem.Builder builder = MenuItem.builder(c)
+                    .setId(id)
+                    .setTitle(title)
+                    .setIcon(icon)
+                    .setOnClickListener(onClickListener)
+                    .setUxRestrictions(uxRestrictions)
+                    .setTinted(tinted)
+                    .setVisible(visible)
+                    .setShowIconAndTitle(showIconAndTitle)
+                    .setDisplayBehavior(displayBehavior);
+
+            if (isSearch) {
+                builder.setToSearch();
+            }
+
+            if (isSettings) {
+                builder.setToSettings();
+            }
+
+            if (checkable || checkedExists) {
+                builder.setChecked(checked);
+            }
+
+            if (activatable || activatedExists) {
+                builder.setActivated(activated);
+            }
+
+            return builder.build();
+        } finally {
+            a.recycle();
+        }
+    }
+}
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
new file mode 100644
index 0000000..7f7eb80
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/SearchView.java
@@ -0,0 +1,241 @@
+/*
+ * 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 static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.car.ui.R;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * A search view used by {@link Toolbar}.
+ */
+public class SearchView extends ConstraintLayout {
+    private final InputMethodManager mInputMethodManager;
+    private final ImageView mIcon;
+    private final EditText mSearchText;
+    private final View mCloseIcon;
+    private final int mStartPaddingWithoutIcon;
+    private final int mStartPadding;
+    private final int mEndPadding;
+    private Set<Toolbar.OnSearchListener> mSearchListeners = Collections.emptySet();
+    private Set<Toolbar.OnSearchCompletedListener> mSearchCompletedListeners =
+            Collections.emptySet();
+    private final TextWatcher mTextWatcher = new TextWatcher() {
+        @Override
+        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+        }
+
+        @Override
+        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+        }
+
+        @Override
+        public void afterTextChanged(Editable editable) {
+            onSearch(editable.toString());
+        }
+    };
+
+    private boolean mIsPlainText = false;
+
+    public SearchView(Context context) {
+        this(context, null);
+    }
+
+    public SearchView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        mInputMethodManager = (InputMethodManager)
+            getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+
+        LayoutInflater inflater = LayoutInflater.from(context);
+        inflater.inflate(R.layout.car_ui_toolbar_search_view, this, true);
+
+        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);
+
+        mStartPaddingWithoutIcon = mSearchText.getPaddingStart();
+        mStartPadding = context.getResources().getDimensionPixelSize(
+                R.dimen.car_ui_toolbar_search_search_icon_container_width);
+        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(
+                (view, hasFocus) -> {
+                    if (hasFocus) {
+                        mInputMethodManager.showSoftInput(view, 0);
+                    } else {
+                        mInputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
+                    }
+                });
+
+        mSearchText.addTextChangedListener(mTextWatcher);
+
+        mSearchText.setOnEditorActionListener((v, actionId, event) -> {
+            if (actionId == EditorInfo.IME_ACTION_DONE
+                    || actionId == EditorInfo.IME_ACTION_SEARCH) {
+                mSearchText.clearFocus();
+                for (Toolbar.OnSearchCompletedListener listener : mSearchCompletedListeners) {
+                    listener.onSearchCompleted();
+                }
+            }
+            return false;
+        });
+    }
+
+    private boolean mWasShown = false;
+
+    @Override
+    public void onVisibilityChanged(@NonNull View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+
+        boolean isShown = isShown();
+        if (isShown && !mWasShown) {
+            boolean hasQuery = mSearchText.getText().length() > 0;
+            mCloseIcon.setVisibility(hasQuery ? View.VISIBLE : View.GONE);
+            mSearchText.requestFocus();
+        }
+        mWasShown = isShown;
+    }
+
+    /**
+     * Adds a listener for the search text changing.
+     * See also {@link #unregisterOnSearchListener(Toolbar.OnSearchListener)}
+     */
+    public void setSearchListeners(Set<Toolbar.OnSearchListener> listeners) {
+        mSearchListeners = listeners;
+    }
+
+    /**
+     * Removes a search listener.
+     * See also {@link #registerOnSearchListener(Toolbar.OnSearchListener)}
+     */
+    public void setSearchCompletedListeners(Set<Toolbar.OnSearchCompletedListener> listeners) {
+        mSearchCompletedListeners = listeners;
+    }
+
+    /**
+     * Sets the search hint.
+     *
+     * @param resId A string resource id of the search hint.
+     */
+    public void setHint(int resId) {
+        mSearchText.setHint(resId);
+    }
+
+    /**
+     * Sets the search hint
+     *
+     * @param hint A CharSequence of the search hint.
+     */
+    public void setHint(CharSequence hint) {
+        mSearchText.setHint(hint);
+    }
+
+    /** Gets the search hint */
+    public CharSequence getHint() {
+        return mSearchText.getHint();
+    }
+
+    /**
+     * Sets a custom icon to display in the search box.
+     */
+    public void setIcon(Drawable d) {
+        if (d == null) {
+            mIcon.setImageResource(R.drawable.car_ui_icon_search);
+        } else {
+            mIcon.setImageDrawable(d);
+        }
+    }
+
+    /**
+     * Sets a custom icon to display in the search box.
+     */
+    public void setIcon(int resId) {
+        if (resId == 0) {
+            mIcon.setImageResource(R.drawable.car_ui_icon_search);
+        } else {
+            mIcon.setImageResource(resId);
+        }
+    }
+
+    /**
+     * Sets whether or not the search bar should look like a regular text box
+     * instead of a search box.
+     */
+    public void setPlainText(boolean plainText) {
+        if (plainText != mIsPlainText) {
+            if (plainText) {
+                mSearchText.setPaddingRelative(mStartPaddingWithoutIcon, 0, mEndPadding, 0);
+                mSearchText.setImeOptions(EditorInfo.IME_ACTION_DONE);
+                mIcon.setVisibility(View.GONE);
+            } else {
+                mSearchText.setPaddingRelative(mStartPadding, 0, mEndPadding, 0);
+                mSearchText.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
+                mIcon.setVisibility(View.VISIBLE);
+            }
+            mIsPlainText = plainText;
+
+            // Needed to detect changes to imeOptions
+            mInputMethodManager.restartInput(mSearchText);
+        }
+    }
+
+    private void onSearch(String query) {
+        mCloseIcon.setVisibility(TextUtils.isEmpty(query) ? View.GONE : View.VISIBLE);
+
+        for (Toolbar.OnSearchListener listener : mSearchListeners) {
+            listener.onSearch(query);
+        }
+    }
+
+    /**
+     * Sets the text being searched.
+     */
+    public void setSearchQuery(String query) {
+        mSearchText.setText(query);
+        mSearchText.setSelection(mSearchText.getText().length());
+    }
+}
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
new file mode 100644
index 0000000..828e54a
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/TabLayout.java
@@ -0,0 +1,329 @@
+/*
+ * 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 static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.util.ArraySet;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+
+import com.android.car.ui.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Custom tab layout which supports adding tabs dynamically
+ *
+ * <p>It supports two layout modes:
+ * <ul><li>Flexible layout which will fill the width
+ * <li>Non-flexible layout which wraps content with a minimum tab width. By setting tab gravity,
+ * it can left aligned, right aligned or center aligned.
+ *
+ * <p>Scrolling function is not supported. If a tab item runs out of the tab layout bound, there
+ * is no way to access it. It's better to set the layout mode to flexible in this case.
+ *
+ * <p>Default tab item inflates from R.layout.car_ui_tab_item, but it also supports custom layout
+ * id, by overlaying R.layout.car_ui_tab_item_layout. By doing this, appearance of tab item view
+ * can be customized.
+ *
+ * <p>Touch feedback is using @android:attr/selectableItemBackground.
+ */
+public class TabLayout extends LinearLayout {
+
+    /**
+     * Listener that listens the tab selection change.
+     */
+    public interface Listener {
+        /** Callback triggered when a tab is selected. */
+        default void onTabSelected(Tab tab) {
+        }
+
+        /** Callback triggered when a tab is unselected. */
+        default void onTabUnselected(Tab tab) {
+        }
+
+        /** Callback triggered when a tab is reselected. */
+        default void onTabReselected(Tab tab) {
+        }
+    }
+
+    private final Set<Listener> mListeners = new ArraySet<>();
+
+    private final TabAdapter mTabAdapter;
+
+    public TabLayout(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public TabLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TabLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        Resources resources = context.getResources();
+
+        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);
+    }
+
+    /**
+     * Add a tab to this layout. The tab will be added at the end of the list. If this is the first
+     * tab to be added it will become the selected tab.
+     */
+    public void addTab(Tab tab) {
+        mTabAdapter.add(tab);
+        // If there is only one tab in the group, set it to be selected.
+        if (mTabAdapter.getCount() == 1) {
+            mTabAdapter.selectTab(0);
+        }
+    }
+
+    /** Set the tab as the current selected tab. */
+    public void selectTab(Tab tab) {
+        mTabAdapter.selectTab(tab);
+    }
+
+    /** Set the tab at given position as the current selected tab. */
+    public void selectTab(int position) {
+        mTabAdapter.selectTab(position);
+    }
+
+    /** Returns how tab items it has. */
+    public int getTabCount() {
+        return mTabAdapter.getCount();
+    }
+
+    /** Returns the position of the given tab. */
+    public int getTabPosition(Tab tab) {
+        return mTabAdapter.getPosition(tab);
+    }
+
+    /** Return the tab at the given position. */
+    public Tab get(int position) {
+        return mTabAdapter.getItem(position);
+    }
+
+    /** Clear all tabs. */
+    public void clearAllTabs() {
+        mTabAdapter.clear();
+    }
+
+    /** Register a {@link Listener}. Same listener will only be registered once. */
+    public void addListener(@NonNull Listener listener) {
+        mListeners.add(listener);
+    }
+
+    /** Unregister a {@link Listener} */
+    public void removeListener(@NonNull Listener listener) {
+        mListeners.remove(listener);
+    }
+
+    private void dispatchOnTabSelected(Tab tab) {
+        for (Listener listener : mListeners) {
+            listener.onTabSelected(tab);
+        }
+    }
+
+    private void dispatchOnTabUnselected(Tab tab) {
+        for (Listener listener : mListeners) {
+            listener.onTabUnselected(tab);
+        }
+    }
+
+    private void dispatchOnTabReselected(Tab tab) {
+        for (Listener listener : mListeners) {
+            listener.onTabReselected(tab);
+        }
+    }
+
+    private void addTabView(View tabView, int position) {
+        addView(tabView, position);
+    }
+
+    private static class TabAdapter extends BaseAdapter {
+        private final Context mContext;
+        private final TabLayout mTabLayout;
+        @LayoutRes
+        private final int mTabItemLayoutRes;
+        private final Typeface mUnselectedTypeface;
+        private final Typeface mSelectedTypeface;
+        private final List<Tab> mTabList;
+
+        private TabAdapter(Context context, @LayoutRes int res, TabLayout tabLayout) {
+            mTabList = new ArrayList<>();
+            mContext = context;
+            mTabItemLayoutRes = res;
+            mTabLayout = tabLayout;
+            mUnselectedTypeface = createStyledTypeface(context,
+                    R.style.TextAppearance_CarUi_Widget_Toolbar_Tab);
+            mSelectedTypeface = createStyledTypeface(context,
+                    R.style.TextAppearance_CarUi_Widget_Toolbar_Tab_Selected);
+        }
+
+        private void add(@NonNull Tab tab) {
+            mTabList.add(tab);
+            notifyItemInserted(mTabList.size() - 1);
+        }
+
+        private void clear() {
+            mTabList.clear();
+            mTabLayout.removeAllViews();
+        }
+
+        private int getPosition(Tab tab) {
+            return mTabList.indexOf(tab);
+        }
+
+        @Override
+        public int getCount() {
+            return mTabList.size();
+        }
+
+        @Override
+        public Tab getItem(int position) {
+            return mTabList.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        @NonNull
+        public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+            View tabItemView = LayoutInflater.from(mContext)
+                    .inflate(mTabItemLayoutRes, parent, false);
+
+            presentTabItemView(position, tabItemView);
+            return tabItemView;
+        }
+
+        private void selectTab(Tab tab) {
+            selectTab(getPosition(tab));
+        }
+
+        private void selectTab(int position) {
+            if (position < 0 || position >= getCount()) {
+                throw new IndexOutOfBoundsException("Invalid position");
+            }
+
+            for (int i = 0; i < getCount(); i++) {
+                Tab tab = mTabList.get(i);
+                boolean isTabSelected = position == i;
+                if (tab.mIsSelected != isTabSelected) {
+                    tab.mIsSelected = isTabSelected;
+                    notifyItemChanged(i);
+                    if (tab.mIsSelected) {
+                        mTabLayout.dispatchOnTabSelected(tab);
+                    } else {
+                        mTabLayout.dispatchOnTabUnselected(tab);
+                    }
+                } else if (tab.mIsSelected) {
+                    mTabLayout.dispatchOnTabReselected(tab);
+                }
+            }
+        }
+
+        /** Represent the tab item at given position without destroying and recreating UI. */
+        private void notifyItemChanged(int position) {
+            View tabItemView = mTabLayout.getChildAt(position);
+            presentTabItemView(position, tabItemView);
+        }
+
+        private void notifyItemInserted(int position) {
+            View insertedView = getView(position, null, mTabLayout);
+            mTabLayout.addTabView(insertedView, position);
+        }
+
+        private void presentTabItemView(int position, @NonNull View tabItemView) {
+            Tab tab = mTabList.get(position);
+
+            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);
+            tab.bindIcon(iconView);
+            tabItemView.setActivated(tab.mIsSelected);
+            textView.setTypeface(tab.mIsSelected ? mSelectedTypeface : mUnselectedTypeface);
+        }
+
+        private static Typeface createStyledTypeface(Context context, @StyleRes int styleResId) {
+            TypedArray ta = context.obtainStyledAttributes(styleResId, new int[] {
+                    android.R.attr.textStyle,
+                    android.R.attr.textFontWeight
+            });
+
+            try {
+                // If not specified, default to 0, which stands for normal.
+                int textStyle = ta.getInteger(0, 0);
+                // If not specified, default value will be 0 which is a light font.
+                int textFontWeight = ta.getInteger(1, 0);
+
+                return Typeface.create(Typeface.defaultFromStyle(textStyle), textFontWeight,
+                        (textStyle & Typeface.ITALIC) != 0);
+            } finally {
+                ta.recycle();
+            }
+        }
+    }
+
+    /** Tab entity. */
+    public static class Tab {
+        private final Drawable mIcon;
+        private final CharSequence mText;
+        private boolean mIsSelected;
+
+        public Tab(@Nullable Drawable icon, @Nullable CharSequence text) {
+            mIcon = icon;
+            mText = text;
+        }
+
+        /** Set tab text. */
+        protected void bindText(TextView textView) {
+            textView.setText(mText);
+        }
+
+        /** Set icon drawable. TODO(b/139444064): revise this api.*/
+        protected void bindIcon(ImageView imageView) {
+            imageView.setImageDrawable(mIcon);
+        }
+    }
+}
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
new file mode 100644
index 0000000..697c783
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
@@ -0,0 +1,603 @@
+/*
+ * 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.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+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 com.android.car.ui.R;
+
+import java.util.List;
+
+/**
+ * A toolbar for Android Automotive OS apps.
+ *
+ * <p>This isn't a toolbar in the android framework sense, it's merely a custom view that can be
+ * added to a layout. (You can't call
+ * {@link android.app.Activity#setActionBar(android.widget.Toolbar)} with it)
+ *
+ * <p>The toolbar supports a navigation button, title, tabs, search, and {@link MenuItem MenuItems}
+ */
+public class Toolbar extends FrameLayout implements ToolbarController {
+
+    /** Callback that will be issued whenever the height of toolbar is changed. */
+    public interface OnHeightChangedListener {
+        /**
+         * Will be called when the height of the toolbar is changed.
+         *
+         * @param height new height of the toolbar
+         */
+        void onHeightChanged(int height);
+    }
+
+    /** Back button listener */
+    public interface OnBackListener {
+        /**
+         * Invoked when the user clicks on the back button. By default, the toolbar will call
+         * the Activity's {@link android.app.Activity#onBackPressed()}. Returning true from
+         * this method will absorb the back press and prevent that behavior.
+         */
+        boolean onBack();
+    }
+
+    /** Tab selection listener */
+    public interface OnTabSelectedListener {
+        /** Called when a {@link TabLayout.Tab} is selected */
+        void onTabSelected(TabLayout.Tab tab);
+    }
+
+    /** Search listener */
+    public interface OnSearchListener {
+        /**
+         * Invoked when the user edits a search query.
+         *
+         * <p>This is called for every letter the user types, and also empty strings if the user
+         * erases everything.
+         */
+        void onSearch(String query);
+    }
+
+    /** Search completed listener */
+    public interface OnSearchCompletedListener {
+        /**
+         * Invoked when the user submits a search query by clicking the keyboard's search / done
+         * button.
+         */
+        void onSearchCompleted();
+    }
+
+    private static final String TAG = "CarUiToolbar";
+
+    /** Enum of states the toolbar can be in. Controls what elements of the toolbar are displayed */
+    public enum State {
+        /**
+         * In the HOME state, the logo will be displayed if there is one, and no navigation icon
+         * will be displayed. The tab bar will be visible. The title will be displayed if there
+         * is space. MenuItems will be displayed.
+         */
+        HOME,
+        /**
+         * In the SUBPAGE state, the logo will be replaced with a back button, the tab bar won't
+         * be visible. The title and MenuItems will be displayed.
+         */
+        SUBPAGE,
+        /**
+         * In the SEARCH state, only the back button and the search bar will be visible.
+         */
+        SEARCH,
+        /**
+         * In the EDIT state, the search bar will look like a regular text box, but will be
+         * functionally identical to the SEARCH state.
+         */
+        EDIT,
+    }
+
+    private ToolbarControllerImpl mController;
+    private boolean mEatingTouch = false;
+    private boolean mEatingHover = false;
+
+    public Toolbar(Context context) {
+        this(context, null);
+    }
+
+    public Toolbar(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.CarUiToolbarStyle);
+    }
+
+    public Toolbar(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        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 {
+            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));
+            String searchHint = a.getString(R.styleable.CarUiToolbar_searchHint);
+            if (searchHint != null) {
+                setSearchHint(searchHint);
+            }
+
+            switch (a.getInt(R.styleable.CarUiToolbar_state, 0)) {
+                case 0:
+                    setState(State.HOME);
+                    break;
+                case 1:
+                    setState(State.SUBPAGE);
+                    break;
+                case 2:
+                    setState(State.SEARCH);
+                    break;
+                default:
+                    if (Log.isLoggable(TAG, Log.WARN)) {
+                        Log.w(TAG, "Unknown initial state");
+                    }
+                    break;
+            }
+
+            switch (a.getInt(R.styleable.CarUiToolbar_navButtonMode, 0)) {
+                case 0:
+                    setNavButtonMode(NavButtonMode.BACK);
+                    break;
+                case 1:
+                    setNavButtonMode(NavButtonMode.CLOSE);
+                    break;
+                case 2:
+                    setNavButtonMode(NavButtonMode.DOWN);
+                    break;
+                default:
+                    if (Log.isLoggable(TAG, Log.WARN)) {
+                        Log.w(TAG, "Unknown navigation button style");
+                    }
+                    break;
+            }
+        } finally {
+            a.recycle();
+        }
+    }
+
+    /**
+     * Override this in a subclass to allow for different toolbar layouts within a single app.
+     *
+     * <p>Non-system apps should not use this, as customising the layout isn't possible with RROs
+     */
+    protected int getToolbarLayout() {
+        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;
+    }
+
+    /**
+     * Returns {@code true} if a two row layout in enabled for the toolbar.
+     */
+    public boolean isTabsInSecondRow() {
+        return mController.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.
+     */
+    public void setTitle(@StringRes int title) {
+        mController.setTitle(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.
+     */
+    public void setTitle(CharSequence title) {
+        mController.setTitle(title);
+    }
+
+    public CharSequence getTitle() {
+        return mController.getTitle();
+    }
+
+    /**
+     * Gets the {@link TabLayout} for this toolbar.
+     */
+    public TabLayout getTabLayout() {
+        return mController.getTabLayout();
+    }
+
+    /**
+     * Adds a tab to this toolbar. You can listen for when it is selected via
+     * {@link #registerOnTabSelectedListener(OnTabSelectedListener)}.
+     */
+    public void addTab(TabLayout.Tab tab) {
+        mController.addTab(tab);
+    }
+
+    /** Removes all the tabs. */
+    public void clearAllTabs() {
+        mController.clearAllTabs();
+    }
+
+    /**
+     * Gets a tab added to this toolbar. See
+     * {@link #addTab(TabLayout.Tab)}.
+     */
+    public TabLayout.Tab getTab(int position) {
+        return mController.getTab(position);
+    }
+
+    /**
+     * Selects a tab added to this toolbar. See
+     * {@link #addTab(TabLayout.Tab)}.
+     */
+    public void selectTab(int 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();
+    }
+
+    /**
+     * 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) {
+        mController.setLogo(resId);
+    }
+
+    /**
+     * 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) {
+        mController.setLogo(drawable);
+    }
+
+    /** Sets the hint for the search bar. */
+    public void setSearchHint(@StringRes int resId) {
+        mController.setSearchHint(resId);
+    }
+
+    /** Sets the hint for the search bar. */
+    public void setSearchHint(CharSequence hint) {
+        mController.setSearchHint(hint);
+    }
+
+    /** Gets the search hint */
+    public CharSequence getSearchHint() {
+        return mController.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.
+     */
+    public void setSearchIcon(@DrawableRes int resId) {
+        mController.setSearchIcon(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) {
+        mController.setSearchIcon(d);
+    }
+
+    /**
+     * An enum of possible styles the nav button could be in. All styles will still call
+     * {@link OnBackListener#onBack()}.
+     */
+    public enum NavButtonMode {
+        /** A back button */
+        BACK,
+        /** A close button */
+        CLOSE,
+        /** A down button, used to indicate that the page will animate down when navigating away */
+        DOWN
+    }
+
+    /** Sets the {@link NavButtonMode} */
+    public void setNavButtonMode(NavButtonMode style) {
+        mController.setNavButtonMode(style);
+    }
+
+    /** Gets the {@link NavButtonMode} */
+    public NavButtonMode getNavButtonMode() {
+        return mController.getNavButtonMode();
+    }
+
+    /**
+     * setBackground is disallowed, to prevent apps from deviating from the intended style too much.
+     */
+    @Override
+    public void setBackground(Drawable d) {
+        throw new UnsupportedOperationException(
+                "You can not change the background of a CarUi toolbar, use "
+                        + "setBackgroundShown(boolean) or an RRO instead.");
+    }
+
+    /** Show/hide the background. When hidden, the toolbar is completely transparent. */
+    public void setBackgroundShown(boolean shown) {
+        mController.setBackgroundShown(shown);
+    }
+
+    /** Returns true is the toolbar background is shown */
+    public boolean getBackgroundShown() {
+        return mController.getBackgroundShown();
+    }
+
+    /**
+     * Sets the {@link MenuItem Menuitems} to display.
+     */
+    public void setMenuItems(@Nullable List<MenuItem> items) {
+        mController.setMenuItems(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>
+     *
+     * @see #setMenuItems(List)
+     * @return The MenuItems that were loaded from XML.
+     */
+    public List<MenuItem> setMenuItems(@XmlRes int resId) {
+        return mController.setMenuItems(resId);
+    }
+
+    /** Gets the {@link MenuItem MenuItems} currently displayed */
+    @NonNull
+    public List<MenuItem> getMenuItems() {
+        return mController.getMenuItems();
+    }
+
+    /** Gets a {@link MenuItem} by id. */
+    @Nullable
+    public MenuItem findMenuItemById(int id) {
+        return mController.findMenuItemById(id);
+    }
+
+    /** Gets a {@link MenuItem} by id. Will throw an exception if not found. */
+    @NonNull
+    public MenuItem requireMenuItemById(int id) {
+        return mController.requireMenuItemById(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.
+     */
+    public void setShowMenuItemsWhileSearching(boolean showMenuItems) {
+        mController.setShowMenuItemsWhileSearching(showMenuItems);
+    }
+
+    /** Returns if {@link MenuItem MenuItems} are shown while searching */
+    public boolean getShowMenuItemsWhileSearching() {
+        return mController.getShowMenuItemsWhileSearching();
+    }
+
+    /**
+     * Sets the search query.
+     */
+    public void setSearchQuery(String query) {
+        mController.setSearchQuery(query);
+    }
+
+    /**
+     * Sets the state of the toolbar. This will show/hide the appropriate elements of the toolbar
+     * for the desired state.
+     */
+    public void setState(State state) {
+        mController.setState(state);
+    }
+
+    /** Gets the current {@link State} of the toolbar. */
+    public State getState() {
+        return mController.getState();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        // Copied from androidx.appcompat.widget.Toolbar
+
+        // Toolbars always eat touch events, but should still respect the touch event dispatch
+        // contract. If the normal View implementation doesn't want the events, we'll just silently
+        // eat the rest of the gesture without reporting the events to the default implementation
+        // since that's what it expects.
+
+        final int action = ev.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            mEatingTouch = false;
+        }
+
+        if (!mEatingTouch) {
+            final boolean handled = super.onTouchEvent(ev);
+            if (action == MotionEvent.ACTION_DOWN && !handled) {
+                mEatingTouch = true;
+            }
+        }
+
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            mEatingTouch = false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent ev) {
+        // Copied from androidx.appcompat.widget.Toolbar
+
+        // Same deal as onTouchEvent() above. Eat all hover events, but still
+        // respect the touch event dispatch contract.
+
+        final int action = ev.getActionMasked();
+        if (action == MotionEvent.ACTION_HOVER_ENTER) {
+            mEatingHover = false;
+        }
+
+        if (!mEatingHover) {
+            final boolean handled = super.onHoverEvent(ev);
+            if (action == MotionEvent.ACTION_HOVER_ENTER && !handled) {
+                mEatingHover = true;
+            }
+        }
+
+        if (action == MotionEvent.ACTION_HOVER_EXIT || action == MotionEvent.ACTION_CANCEL) {
+            mEatingHover = false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Registers a new {@link 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(
+            OnHeightChangedListener listener) {
+        mController.registerToolbarHeightChangeListener(listener);
+    }
+
+    /** Unregisters an existing {@link OnHeightChangedListener} from the list of listeners. */
+    public boolean unregisterToolbarHeightChangeListener(
+            OnHeightChangedListener listener) {
+        return mController.unregisterToolbarHeightChangeListener(listener);
+    }
+
+    /** Registers a new {@link OnTabSelectedListener} to the list of listeners. */
+    public void registerOnTabSelectedListener(OnTabSelectedListener listener) {
+        mController.registerOnTabSelectedListener(listener);
+    }
+
+    /** Unregisters an existing {@link OnTabSelectedListener} from the list of listeners. */
+    public boolean unregisterOnTabSelectedListener(OnTabSelectedListener listener) {
+        return mController.unregisterOnTabSelectedListener(listener);
+    }
+
+    /** Registers a new {@link OnSearchListener} to the list of listeners. */
+    public void registerOnSearchListener(OnSearchListener listener) {
+        mController.registerOnSearchListener(listener);
+    }
+
+    /** Unregisters an existing {@link OnSearchListener} from the list of listeners. */
+    public boolean unregisterOnSearchListener(OnSearchListener listener) {
+        return mController.unregisterOnSearchListener(listener);
+    }
+
+    /** Registers a new {@link OnSearchCompletedListener} to the list of listeners. */
+    public void registerOnSearchCompletedListener(OnSearchCompletedListener listener) {
+        mController.registerOnSearchCompletedListener(listener);
+    }
+
+    /** Unregisters an existing {@link OnSearchCompletedListener} from the list of listeners. */
+    public boolean unregisterOnSearchCompletedListener(OnSearchCompletedListener listener) {
+        return mController.unregisterOnSearchCompletedListener(listener);
+    }
+
+    /** Registers a new {@link OnBackListener} to the list of listeners. */
+    public void registerOnBackListener(OnBackListener listener) {
+        mController.registerOnBackListener(listener);
+    }
+
+    /** Unregisters an existing {@link OnBackListener} from the list of listeners. */
+    public boolean unregisterOnBackListener(OnBackListener listener) {
+        return mController.unregisterOnBackListener(listener);
+    }
+
+    /** Shows the progress bar */
+    public void showProgressBar() {
+        mController.showProgressBar();
+    }
+
+    /** Hides the progress bar */
+    public void hideProgressBar() {
+        mController.hideProgressBar();
+    }
+
+    /** Returns the progress bar */
+    public ProgressBar getProgressBar() {
+        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
new file mode 100644
index 0000000..54d64f1
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
@@ -0,0 +1,121 @@
+/*
+ * 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.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+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
+ */
+public final class CarUiUtils {
+    /** This is a utility class */
+    private CarUiUtils() {}
+
+    /**
+     * Reads a float value from a dimens resource. This is necessary as {@link Resources#getFloat}
+     * is not currently public.
+     *
+     * @param res {@link Resources} to read values from
+     * @param resId Id of the dimens resource to read
+     */
+    public static float getFloat(Resources res, @DimenRes int resId) {
+        TypedValue outValue = new TypedValue();
+        res.getValue(resId, outValue, true);
+        return outValue.getFloat();
+    }
+
+    /** Returns the identifier of the resolved resource assigned to the given attribute. */
+    public static int getAttrResourceId(Context context, int attr) {
+        return getAttrResourceId(context, /*styleResId=*/ 0, attr);
+    }
+
+    /**
+     * Returns the identifier of the resolved resource assigned to the given attribute defined in
+     * the given style.
+     */
+    public static int getAttrResourceId(Context context, @StyleRes int styleResId, int attr) {
+        TypedArray ta = context.obtainStyledAttributes(styleResId, new int[]{attr});
+        int resId = ta.getResourceId(0, 0);
+        ta.recycle();
+        return resId;
+    }
+
+    /**
+     * Gets the {@link Activity} for a certain {@link Context}.
+     *
+     * <p>It is possible the Context is not associated with an Activity, in which case
+     * this method will return null.
+     */
+    @Nullable
+    public static Activity getActivity(Context context) {
+        while (context instanceof ContextWrapper) {
+            if (context instanceof Activity) {
+                return (Activity) context;
+            }
+            context = ((ContextWrapper) context).getBaseContext();
+        }
+        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/src/com/android/car/ui/utils/CarUxRestrictionsUtil.java b/car-ui-lib/src/com/android/car/ui/utils/CarUxRestrictionsUtil.java
new file mode 100644
index 0000000..31ac24a
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/utils/CarUxRestrictionsUtil.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 com.android.car.ui.utils;
+
+import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_LIMIT_STRING_LENGTH;
+
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo;
+import android.car.drivingstate.CarUxRestrictionsManager;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.car.ui.R;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Utility class to access Car Restriction Manager.
+ *
+ * <p>This class must be a singleton because only one listener can be registered with {@link
+ * CarUxRestrictionsManager} at a time, as documented in {@link
+ * CarUxRestrictionsManager#registerListener}.
+ */
+public class CarUxRestrictionsUtil {
+    private static final String TAG = "CarUxRestrictionsUtil";
+
+    private final Car mCarApi;
+    private CarUxRestrictionsManager mCarUxRestrictionsManager;
+    @NonNull
+    private CarUxRestrictions mCarUxRestrictions = getDefaultRestrictions();
+
+    private Set<OnUxRestrictionsChangedListener> mObservers;
+    private static CarUxRestrictionsUtil sInstance = null;
+
+    private CarUxRestrictionsUtil(Context context) {
+        CarUxRestrictionsManager.OnUxRestrictionsChangedListener listener =
+                (carUxRestrictions) -> {
+                    if (carUxRestrictions == null) {
+                        mCarUxRestrictions = getDefaultRestrictions();
+                    } else {
+                        mCarUxRestrictions = carUxRestrictions;
+                    }
+
+                    for (OnUxRestrictionsChangedListener observer : mObservers) {
+                        observer.onRestrictionsChanged(mCarUxRestrictions);
+                    }
+                };
+
+        mCarApi = Car.createCar(context.getApplicationContext());
+        mObservers = Collections.newSetFromMap(new WeakHashMap<>());
+
+        try {
+            mCarUxRestrictionsManager =
+                    (CarUxRestrictionsManager) mCarApi.getCarManager(
+                            Car.CAR_UX_RESTRICTION_SERVICE);
+            mCarUxRestrictionsManager.registerListener(listener);
+            listener.onUxRestrictionsChanged(
+                    mCarUxRestrictionsManager.getCurrentCarUxRestrictions());
+        } catch (CarNotConnectedException | NullPointerException e) {
+            Log.e(TAG, "Car not connected", e);
+            // mCarUxRestrictions will be the default
+        }
+    }
+
+    @NonNull
+    private static CarUxRestrictions getDefaultRestrictions() {
+        return new CarUxRestrictions.Builder(
+                true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED, 0)
+                .build();
+    }
+
+    /** Listener interface used to update clients on UxRestrictions changes */
+    public interface OnUxRestrictionsChangedListener {
+        /** Called when CarUxRestrictions changes */
+        void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions);
+    }
+
+    /** Returns the singleton sInstance of this class */
+    @NonNull
+    public static CarUxRestrictionsUtil getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new CarUxRestrictionsUtil(context);
+        }
+
+        return sInstance;
+    }
+
+    /**
+     * Registers a listener on this class for updates to CarUxRestrictions. Multiple listeners may
+     * be registered. Note that this class will only hold a weak reference to the listener, you
+     * must maintain a strong reference to it elsewhere.
+     */
+    public void register(OnUxRestrictionsChangedListener listener) {
+        mObservers.add(listener);
+        listener.onRestrictionsChanged(mCarUxRestrictions);
+    }
+
+    /** Unregisters a registered listener */
+    public void unregister(OnUxRestrictionsChangedListener listener) {
+        mObservers.remove(listener);
+    }
+
+    @NonNull
+    public CarUxRestrictions getCurrentRestrictions() {
+        return mCarUxRestrictions;
+    }
+
+    /**
+     * Returns whether any of the given flags are blocked by the specified restrictions. If null is
+     * given, the method returns true for safety.
+     */
+    public static boolean isRestricted(
+            @CarUxRestrictionsInfo int restrictionFlags, @Nullable CarUxRestrictions uxr) {
+        return (uxr == null) || ((uxr.getActiveRestrictions() & restrictionFlags) != 0);
+    }
+
+    /**
+     * Complies the input string with the given UX restrictions. Returns the original string if
+     * already compliant, otherwise a shortened ellipsized string.
+     */
+    public static String complyString(Context context, String str, CarUxRestrictions uxr) {
+
+        if (isRestricted(UX_RESTRICTIONS_LIMIT_STRING_LENGTH, uxr)) {
+            int maxLength =
+                    uxr == null
+                            ? context.getResources().getInteger(
+                            R.integer.car_ui_default_max_string_length)
+                            : uxr.getMaxRestrictedStringLength();
+
+            if (str.length() > maxLength) {
+                return str.substring(0, maxLength) + context.getString(R.string.car_ui_ellipsis);
+            }
+        }
+
+        return str;
+    }
+
+    /** Sets car UX restrictions. Only used for testing. */
+    @VisibleForTesting
+    public void setUxRestrictions(CarUxRestrictions carUxRestrictions) {
+        mCarUxRestrictions = carUxRestrictions;
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/uxr/DrawableStateButton.java b/car-ui-lib/src/com/android/car/ui/uxr/DrawableStateButton.java
new file mode 100644
index 0000000..9e5e1ce
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/uxr/DrawableStateButton.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.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link Button} that implements {@link DrawableStateView}, for allowing additional states
+ * such as ux restriction.
+ */
+public class DrawableStateButton extends Button implements DrawableStateView {
+
+    private int[] mState;
+
+    public DrawableStateButton(Context context) {
+        super(context);
+    }
+
+    public DrawableStateButton(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DrawableStateButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public DrawableStateButton(
+            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public void setDrawableState(int[] state) {
+        mState = state;
+        refreshDrawableState();
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        if (mState == null) {
+            return super.onCreateDrawableState(extraSpace);
+        } else {
+            return mergeDrawableStates(
+                    super.onCreateDrawableState(extraSpace + mState.length), mState);
+        }
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/uxr/DrawableStateSwitch.java b/car-ui-lib/src/com/android/car/ui/uxr/DrawableStateSwitch.java
new file mode 100644
index 0000000..bfa018c
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/uxr/DrawableStateSwitch.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.ui.uxr;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Switch;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A {@link Switch} that implements {@link DrawableStateView}, for allowing additional states
+ * such as ux restriction.
+ */
+public class DrawableStateSwitch extends Switch implements DrawableStateView {
+    private int[] mState;
+
+    public DrawableStateSwitch(Context context) {
+        super(context);
+    }
+
+    public DrawableStateSwitch(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DrawableStateSwitch(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public DrawableStateSwitch(
+            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public void setDrawableState(int[] state) {
+        mState = state;
+        refreshDrawableState();
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        if (mState == null) {
+            return super.onCreateDrawableState(extraSpace);
+        } else {
+            return mergeDrawableStates(
+                    super.onCreateDrawableState(extraSpace + mState.length), mState);
+        }
+    }
+}
diff --git a/car-ui-lib/src/com/android/car/ui/uxr/DrawableStateView.java b/car-ui-lib/src/com/android/car/ui/uxr/DrawableStateView.java
new file mode 100644
index 0000000..a9cdb52
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/uxr/DrawableStateView.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.ui.uxr;
+
+/**
+ * An Interface to expose a view's drawable state.
+ *
+ * <p>Used by {@link com.android.car.ui.toolbar.Toolbar Toolbar's}
+ * {@link com.android.car.ui.toolbar.MenuItem MenuItems} to make the views display if they are ux
+ * restricted.
+ */
+public interface DrawableStateView {
+    /** Sets the drawable state. This should merge with existing drawable states */
+    void setDrawableState(int[] state);
+}
diff --git a/car-ui-lib/tests/Android.mk b/car-ui-lib/tests/Android.mk
new file mode 100644
index 0000000..9f0a4e8
--- /dev/null
+++ b/car-ui-lib/tests/Android.mk
@@ -0,0 +1,19 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+# Include all makefiles in subdirectories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/car-ui-lib/tests/apitest/Android.mk b/car-ui-lib/tests/apitest/Android.mk
new file mode 100644
index 0000000..b4b56dd
--- /dev/null
+++ b/car-ui-lib/tests/apitest/Android.mk
@@ -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.
+#
+####################################################################
+# Car Ui test mapping target to check resources. #
+####################################################################
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+intermediates := $(call intermediates-dir-for, PACKAGING, Android,,COMMON)
+car-ui-stamp := $(intermediates)/car-ui-stamp
+script := $(LOCAL_PATH)/auto-generate-resources.py
+
+$(car-ui-stamp): PRIVATE_SCRIPT := $(script)
+$(car-ui-stamp) : $(script)
+	python $(PRIVATE_SCRIPT) -f=updated-resources.xml -c
+	$(hide) mkdir -p $(dir $@) && touch $@
+
+.PHONY: generate-resources
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+generate-resources : $(car-ui-stamp)
diff --git a/car-ui-lib/tests/apitest/auto-generate-resources.py b/car-ui-lib/tests/apitest/auto-generate-resources.py
new file mode 100755
index 0000000..9645ef5
--- /dev/null
+++ b/car-ui-lib/tests/apitest/auto-generate-resources.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+#
+# 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.
+
+import argparse
+import os
+import sys
+from resource_utils import get_all_resources, get_resources_from_single_file, remove_layout_resources
+from git_utils import has_chassis_changes
+
+# path to 'packages/apps/Car/libs/car-ui-lib/'
+ROOT_FOLDER = os.path.dirname(os.path.abspath(__file__)) + '/../..'
+OUTPUT_FILE_PATH = ROOT_FOLDER + '/tests/apitest/'
+
+"""
+Script used to update the 'current.xml' file. This is being used as part of pre-submits to
+verify whether resources previously exposed to OEMs are being changed by a CL, potentially
+breaking existing customizations.
+
+Example usage: python auto-generate-resources.py current.xml
+"""
+def main():
+    parser = argparse.ArgumentParser(description='Check if any existing resources are modified.')
+    parser.add_argument('--sha', help='Git hash of current changes. This script will not run if this is provided and there are no chassis changes.')
+    parser.add_argument('-f', '--file', default='current.xml', help='Name of output file.')
+    parser.add_argument('-c', '--compare', action='store_true',
+                        help='Pass this flag if resources need to be compared.')
+    args = parser.parse_args()
+
+    if not has_chassis_changes(args.sha):
+        # Don't run because there were no chassis changes
+        return
+
+    output_file = args.file or 'current.xml'
+    if args.compare:
+        compare_resources(ROOT_FOLDER+'/res', OUTPUT_FILE_PATH + 'current.xml')
+    else:
+        generate_current_file(ROOT_FOLDER+'/res', output_file)
+
+def generate_current_file(res_folder, output_file='current.xml'):
+    resources = remove_layout_resources(get_all_resources(res_folder))
+    resources = sorted(resources, key=lambda x: x.type + x.name)
+
+    # defer importing lxml to here so that people who aren't editing chassis don't have to have
+    # lxml installed
+    import lxml.etree as etree
+
+    root = etree.Element('resources')
+
+    root.addprevious(etree.Comment('This file is AUTO GENERATED, DO NOT EDIT MANUALLY.'))
+    for resource in resources:
+        item = etree.SubElement(root, 'public')
+        item.set('type', resource.type)
+        item.set('name', resource.name)
+
+    data = etree.ElementTree(root)
+
+    with open(OUTPUT_FILE_PATH + output_file, 'w') as f:
+        data.write(f, pretty_print=True, xml_declaration=True, encoding='utf-8')
+
+def compare_resources(res_folder, res_public_file):
+    old_mapping = get_resources_from_single_file(res_public_file)
+
+    new_mapping = remove_layout_resources(get_all_resources(res_folder))
+
+    removed = old_mapping.difference(new_mapping)
+    added = new_mapping.difference(old_mapping)
+    if len(removed) > 0:
+        print('Resources removed:\n' + '\n'.join(map(lambda x: str(x), removed)))
+    if len(added) > 0:
+        print('Resources added:\n' + '\n'.join(map(lambda x: str(x), added)))
+
+    if len(added) + len(removed) > 0:
+        print("Some resource have been modified. If this is intentional please " +
+              "run 'python auto-generate-resources.py' again and submit the new current.xml")
+        sys.exit(1)
+
+if __name__ == '__main__':
+    main()
diff --git a/car-ui-lib/tests/apitest/current.xml b/car-ui-lib/tests/apitest/current.xml
new file mode 100644
index 0000000..3856c54
--- /dev/null
+++ b/car-ui-lib/tests/apitest/current.xml
@@ -0,0 +1,278 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!--This file is AUTO GENERATED, DO NOT EDIT MANUALLY.-->
+<resources>
+  <public type="attr" name="CarUiToolbarStyle"/>
+  <public type="attr" name="carUiPreferenceStyle"/>
+  <public type="attr" name="carUiRecyclerViewStyle"/>
+  <public type="attr" name="state_ux_restricted"/>
+  <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_enable"/>
+  <public type="bool" name="car_ui_toolbar_logo_fills_nav_icon_space"/>
+  <public type="bool" name="car_ui_toolbar_nav_icon_reserve_space"/>
+  <public type="bool" name="car_ui_toolbar_show_logo"/>
+  <public type="bool" name="car_ui_toolbar_tab_flexible_layout"/>
+  <public type="bool" name="car_ui_toolbar_tabs_on_second_row"/>
+  <public type="color" name="car_ui_activity_background_color"/>
+  <public type="color" name="car_ui_color_accent"/>
+  <public type="color" name="car_ui_list_item_body_text_color"/>
+  <public type="color" name="car_ui_list_item_divider"/>
+  <public type="color" name="car_ui_list_item_header_text_color"/>
+  <public type="color" name="car_ui_list_item_title_text_color"/>
+  <public type="color" name="car_ui_preference_category_title_text_color"/>
+  <public type="color" name="car_ui_preference_edit_text_dialog_message_text_color"/>
+  <public type="color" name="car_ui_preference_icon_color"/>
+  <public type="color" name="car_ui_preference_summary_text_color"/>
+  <public type="color" name="car_ui_preference_switch_track_text_color"/>
+  <public type="color" name="car_ui_preference_title_text_color"/>
+  <public type="color" name="car_ui_recyclerview_divider_color"/>
+  <public type="color" name="car_ui_ripple_color"/>
+  <public type="color" name="car_ui_scrollbar_thumb"/>
+  <public type="color" name="car_ui_text_color_hint"/>
+  <public type="color" name="car_ui_text_color_primary"/>
+  <public type="color" name="car_ui_text_color_secondary"/>
+  <public type="color" name="car_ui_toolbar_menu_item_icon_background_color"/>
+  <public type="color" name="car_ui_toolbar_menu_item_icon_color"/>
+  <public type="color" name="car_ui_toolbar_nav_icon_color"/>
+  <public type="color" name="car_ui_toolbar_search_hint_text_color"/>
+  <public type="color" name="car_ui_toolbar_tab_item_selector"/>
+  <public type="color" name="car_ui_toolbar_tab_selected_color"/>
+  <public type="color" name="car_ui_toolbar_tab_unselected_color"/>
+  <public type="dimen" name="car_ui_body1_size"/>
+  <public type="dimen" name="car_ui_body2_size"/>
+  <public type="dimen" name="car_ui_body3_size"/>
+  <public type="dimen" name="car_ui_button_disabled_alpha"/>
+  <public type="dimen" name="car_ui_dialog_edittext_height"/>
+  <public type="dimen" name="car_ui_dialog_edittext_margin_bottom"/>
+  <public type="dimen" name="car_ui_dialog_edittext_margin_end"/>
+  <public type="dimen" name="car_ui_dialog_edittext_margin_start"/>
+  <public type="dimen" name="car_ui_dialog_edittext_margin_top"/>
+  <public type="dimen" name="car_ui_dialog_icon_size"/>
+  <public type="dimen" name="car_ui_dialog_title_margin"/>
+  <public type="dimen" name="car_ui_keyline_1"/>
+  <public type="dimen" name="car_ui_keyline_2"/>
+  <public type="dimen" name="car_ui_keyline_3"/>
+  <public type="dimen" name="car_ui_keyline_4"/>
+  <public type="dimen" name="car_ui_letter_spacing_body1"/>
+  <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"/>
+  <public type="dimen" name="car_ui_list_item_header_text_size"/>
+  <public type="dimen" name="car_ui_list_item_height"/>
+  <public type="dimen" name="car_ui_list_item_icon_container_width"/>
+  <public type="dimen" name="car_ui_list_item_icon_size"/>
+  <public type="dimen" name="car_ui_list_item_radio_button_end_inset"/>
+  <public type="dimen" name="car_ui_list_item_radio_button_height"/>
+  <public type="dimen" name="car_ui_list_item_radio_button_icon_container_width"/>
+  <public type="dimen" name="car_ui_list_item_radio_button_start_inset"/>
+  <public type="dimen" name="car_ui_list_item_start_inset"/>
+  <public type="dimen" name="car_ui_list_item_supplemental_icon_size"/>
+  <public type="dimen" name="car_ui_list_item_text_no_icon_start_margin"/>
+  <public type="dimen" name="car_ui_list_item_text_start_margin"/>
+  <public type="dimen" name="car_ui_list_item_title_text_size"/>
+  <public type="dimen" name="car_ui_margin"/>
+  <public type="dimen" name="car_ui_padding_0"/>
+  <public type="dimen" name="car_ui_padding_1"/>
+  <public type="dimen" name="car_ui_padding_2"/>
+  <public type="dimen" name="car_ui_padding_3"/>
+  <public type="dimen" name="car_ui_padding_4"/>
+  <public type="dimen" name="car_ui_padding_5"/>
+  <public type="dimen" name="car_ui_padding_6"/>
+  <public type="dimen" name="car_ui_preference_category_icon_margin_end"/>
+  <public type="dimen" name="car_ui_preference_category_icon_size"/>
+  <public type="dimen" name="car_ui_preference_category_min_height"/>
+  <public type="dimen" name="car_ui_preference_category_text_size"/>
+  <public type="dimen" name="car_ui_preference_content_margin_bottom"/>
+  <public type="dimen" name="car_ui_preference_content_margin_top"/>
+  <public type="dimen" name="car_ui_preference_dropdown_padding_start"/>
+  <public type="dimen" name="car_ui_preference_edit_text_dialog_margin_bottom"/>
+  <public type="dimen" name="car_ui_preference_edit_text_dialog_margin_top"/>
+  <public type="dimen" name="car_ui_preference_edit_text_dialog_message_margin_bottom"/>
+  <public type="dimen" name="car_ui_preference_edit_text_dialog_message_margin_end"/>
+  <public type="dimen" name="car_ui_preference_edit_text_dialog_message_margin_start"/>
+  <public type="dimen" name="car_ui_preference_edit_text_dialog_message_text_size"/>
+  <public type="dimen" name="car_ui_preference_edit_text_dialog_text_margin_end"/>
+  <public type="dimen" name="car_ui_preference_edit_text_dialog_text_margin_start"/>
+  <public type="dimen" name="car_ui_preference_icon_margin_end"/>
+  <public type="dimen" name="car_ui_preference_icon_size"/>
+  <public type="dimen" name="car_ui_preference_summary_text_size"/>
+  <public type="dimen" name="car_ui_preference_switch_height"/>
+  <public type="dimen" name="car_ui_preference_switch_text_size"/>
+  <public type="dimen" name="car_ui_preference_switch_width"/>
+  <public type="dimen" name="car_ui_preference_switch_width_half"/>
+  <public type="dimen" name="car_ui_preference_title_text_size"/>
+  <public type="dimen" name="car_ui_primary_icon_size"/>
+  <public type="dimen" name="car_ui_recyclerview_divider_bottom_margin"/>
+  <public type="dimen" name="car_ui_recyclerview_divider_end_margin"/>
+  <public type="dimen" name="car_ui_recyclerview_divider_height"/>
+  <public type="dimen" name="car_ui_recyclerview_divider_start_margin"/>
+  <public type="dimen" name="car_ui_recyclerview_divider_top_margin"/>
+  <public type="dimen" name="car_ui_scrollbar_button_size"/>
+  <public type="dimen" name="car_ui_scrollbar_container_width"/>
+  <public type="dimen" name="car_ui_scrollbar_decelerate_interpolator_factor"/>
+  <public type="dimen" name="car_ui_scrollbar_deceleration_times_divisor"/>
+  <public type="dimen" name="car_ui_scrollbar_margin"/>
+  <public type="dimen" name="car_ui_scrollbar_milliseconds_per_inch"/>
+  <public type="dimen" name="car_ui_scrollbar_padding_end"/>
+  <public type="dimen" name="car_ui_scrollbar_padding_start"/>
+  <public type="dimen" name="car_ui_scrollbar_separator_margin"/>
+  <public type="dimen" name="car_ui_scrollbar_thumb_radius"/>
+  <public type="dimen" name="car_ui_scrollbar_thumb_width"/>
+  <public type="dimen" name="car_ui_toolbar_bottom_inset"/>
+  <public type="dimen" name="car_ui_toolbar_bottom_view_height"/>
+  <public type="dimen" name="car_ui_toolbar_end_inset"/>
+  <public type="dimen" name="car_ui_toolbar_first_row_height"/>
+  <public type="dimen" name="car_ui_toolbar_logo_size"/>
+  <public type="dimen" name="car_ui_toolbar_margin"/>
+  <public type="dimen" name="car_ui_toolbar_menu_item_icon_background_size"/>
+  <public type="dimen" name="car_ui_toolbar_menu_item_icon_ripple_radius"/>
+  <public type="dimen" name="car_ui_toolbar_menu_item_icon_size"/>
+  <public type="dimen" name="car_ui_toolbar_menu_item_margin"/>
+  <public type="dimen" name="car_ui_toolbar_nav_icon_size"/>
+  <public type="dimen" name="car_ui_toolbar_row_height"/>
+  <public type="dimen" name="car_ui_toolbar_search_close_icon_container_width"/>
+  <public type="dimen" name="car_ui_toolbar_search_close_icon_size"/>
+  <public type="dimen" name="car_ui_toolbar_search_height"/>
+  <public type="dimen" name="car_ui_toolbar_search_search_icon_container_width"/>
+  <public type="dimen" name="car_ui_toolbar_search_search_icon_size"/>
+  <public type="dimen" name="car_ui_toolbar_second_row_height"/>
+  <public type="dimen" name="car_ui_toolbar_separator_height"/>
+  <public type="dimen" name="car_ui_toolbar_start_inset"/>
+  <public type="dimen" name="car_ui_toolbar_tab_icon_height"/>
+  <public type="dimen" name="car_ui_toolbar_tab_icon_width"/>
+  <public type="dimen" name="car_ui_toolbar_tab_padding_x"/>
+  <public type="dimen" name="car_ui_toolbar_tab_text_width"/>
+  <public type="dimen" name="car_ui_toolbar_title_logo_padding"/>
+  <public type="dimen" name="car_ui_toolbar_title_margin_start"/>
+  <public type="dimen" name="car_ui_toolbar_top_inset"/>
+  <public type="dimen" name="car_ui_touch_target_height"/>
+  <public type="dimen" name="car_ui_touch_target_width"/>
+  <public type="dimen" name="wrap_content"/>
+  <public type="drawable" name="car_ui_activity_background"/>
+  <public type="drawable" name="car_ui_divider"/>
+  <public type="drawable" name="car_ui_icon_arrow_back"/>
+  <public type="drawable" name="car_ui_icon_close"/>
+  <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"/>
+  <public type="drawable" name="car_ui_recyclerview_ic_up"/>
+  <public type="drawable" name="car_ui_recyclerview_scrollbar_thumb"/>
+  <public type="drawable" name="car_ui_toolbar_background"/>
+  <public type="drawable" name="car_ui_toolbar_menu_item_divider"/>
+  <public type="drawable" name="car_ui_toolbar_menu_item_icon_background"/>
+  <public type="drawable" name="car_ui_toolbar_menu_item_icon_ripple"/>
+  <public type="drawable" name="car_ui_toolbar_search_close_icon"/>
+  <public type="drawable" name="car_ui_toolbar_search_search_icon"/>
+  <public type="id" name="search"/>
+  <public type="integer" name="car_ui_default_max_string_length"/>
+  <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"/>
+  <public type="string" name="car_ui_ellipsis"/>
+  <public type="string" name="car_ui_list_item_header_font_family"/>
+  <public type="string" name="car_ui_preference_category_title_font_family"/>
+  <public type="string" name="car_ui_preference_switch_off"/>
+  <public type="string" name="car_ui_preference_switch_on"/>
+  <public type="string" name="car_ui_restricted_while_driving"/>
+  <public type="string" name="car_ui_scrollbar_component"/>
+  <public type="string" name="car_ui_scrollbar_page_down_button"/>
+  <public type="string" name="car_ui_scrollbar_page_up_button"/>
+  <public type="string" name="car_ui_toolbar_default_search_hint"/>
+  <public type="string" name="car_ui_toolbar_menu_item_overflow_title"/>
+  <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"/>
+  <public type="style" name="Preference.CarUi.DialogPreference"/>
+  <public type="style" name="Preference.CarUi.DialogPreference.EditTextPreference"/>
+  <public type="style" name="Preference.CarUi.DropDown"/>
+  <public type="style" name="Preference.CarUi.Icon"/>
+  <public type="style" name="Preference.CarUi.Information"/>
+  <public type="style" name="Preference.CarUi.Preference"/>
+  <public type="style" name="Preference.CarUi.PreferenceScreen"/>
+  <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"/>
+  <public type="style" name="TextAppearance.CarUi.PreferenceCategoryTitle"/>
+  <public type="style" name="TextAppearance.CarUi.PreferenceEditTextDialogMessage"/>
+  <public type="style" name="TextAppearance.CarUi.PreferenceSummary"/>
+  <public type="style" name="TextAppearance.CarUi.PreferenceTitle"/>
+  <public type="style" name="TextAppearance.CarUi.Widget"/>
+  <public type="style" name="TextAppearance.CarUi.Widget.Toolbar"/>
+  <public type="style" name="TextAppearance.CarUi.Widget.Toolbar.Tab"/>
+  <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"/>
+  <public type="style" name="Widget.CarUi.AlertDialog.Icon"/>
+  <public type="style" name="Widget.CarUi.AlertDialog.TitleContainer"/>
+  <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.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"/>
+  <public type="style" name="Widget.CarUi.Toolbar.Search.CloseIcon"/>
+  <public type="style" name="Widget.CarUi.Toolbar.Search.EditText"/>
+  <public type="style" name="Widget.CarUi.Toolbar.Search.SearchIcon"/>
+  <public type="style" name="Widget.CarUi.Toolbar.SeparatorView"/>
+  <public type="style" name="Widget.CarUi.Toolbar.Tab"/>
+  <public type="style" name="Widget.CarUi.Toolbar.Tab.Container"/>
+  <public type="style" name="Widget.CarUi.Toolbar.Tab.Icon"/>
+  <public type="style" name="Widget.CarUi.Toolbar.Tab.Text"/>
+  <public type="style" name="Widget.CarUi.Toolbar.TextButton"/>
+  <public type="style" name="Widget.CarUi.Toolbar.TextButton.WithIcon"/>
+  <public type="style" name="Widget.CarUi.Toolbar.Title"/>
+</resources>
diff --git a/car-ui-lib/tests/apitest/git_utils.py b/car-ui-lib/tests/apitest/git_utils.py
new file mode 100755
index 0000000..d3b83a5
--- /dev/null
+++ b/car-ui-lib/tests/apitest/git_utils.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+#
+# 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.
+
+import subprocess
+
+def has_chassis_changes(sha):
+    if sha is None:
+        return True
+
+    result = subprocess.Popen(['git', 'diff-tree', '--no-commit-id', '--name-only', '-r', sha],
+                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    stdout, stderr = result.communicate()
+
+    if result.returncode != 0:
+        raise Exception("Git error: "+str(stdout)+str(stderr))
+
+    return 'car-ui-lib' in str(stdout)
diff --git a/car-ui-lib/tests/apitest/resource_utils.py b/car-ui-lib/tests/apitest/resource_utils.py
new file mode 100755
index 0000000..ab0de6d
--- /dev/null
+++ b/car-ui-lib/tests/apitest/resource_utils.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+#
+# 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.
+
+import os
+
+class ResourceLocation:
+    def __init__(self, file, line=None):
+        self.file = file
+        self.line = line
+
+    def __str__(self):
+        if self.line is not None:
+            return self.file + ':' + str(self.line)
+        else:
+            return self.file
+
+class Resource:
+    def __init__(self, name, type, location=None):
+        self.name = name
+        self.type = type
+        self.locations = []
+        if location is not None:
+            self.locations.append(location)
+
+    def __eq__(self, other):
+        if isinstance(other, _Grab):
+            return other == self
+        return self.name == other.name and self.type == other.type
+
+    def __ne__(self, other):
+        if isinstance(other, _Grab):
+            return other != self
+        return self.name != other.name or self.type != other.type
+
+    def __hash__(self):
+        return hash((self.name, self.type))
+
+    def __str__(self):
+        result = ''
+        for location in self.locations:
+            result += str(location) + ': '
+        result += '<'+self.type+' name="'+self.name+'"'
+
+        return result + '>'
+
+    def __repr__(self):
+        return str(self)
+
+def get_all_resources(resDir):
+    allResDirs = [f for f in os.listdir(resDir) if os.path.isdir(os.path.join(resDir, f))]
+    valuesDirs = [f for f in allResDirs if f.startswith('values')]
+    fileDirs = [f for f in allResDirs if not f.startswith('values')]
+
+    resources = set()
+
+    # Get the filenames of the all the files in all the fileDirs
+    for dir in fileDirs:
+        type = dir.split('-')[0]
+        for file in os.listdir(os.path.join(resDir, dir)):
+            if file.endswith('.xml'):
+                add_resource_to_set(resources,
+                                    Resource(file[:-4], type,
+                                             ResourceLocation(os.path.join(resDir, dir, file))))
+
+    for dir in valuesDirs:
+        for file in os.listdir(os.path.join(resDir, dir)):
+            if file.endswith('.xml'):
+                for resource in get_resources_from_single_file(os.path.join(resDir, dir, file)):
+                    add_resource_to_set(resources, resource)
+
+    return resources
+
+def get_resources_from_single_file(filename):
+    # defer importing lxml to here so that people who aren't editing chassis don't have to have
+    # lxml installed
+    import lxml.etree as etree
+    doc = etree.parse(filename)
+    resourceTag = doc.getroot()
+    result = set()
+    for resource in resourceTag:
+        if resource.tag == 'declare-styleable' or resource.tag is etree.Comment:
+            continue
+
+        if resource.tag == 'item' or resource.tag == 'public':
+            add_resource_to_set(result, Resource(resource.get('name'), resource.get('type'),
+                                                 ResourceLocation(filename, resource.sourceline)))
+        else:
+            add_resource_to_set(result, Resource(resource.get('name'), resource.tag,
+                                                 ResourceLocation(filename, resource.sourceline)))
+    return result
+
+def remove_layout_resources(resourceSet):
+    result = set()
+    for resource in resourceSet:
+        if resource.type != 'layout':
+            result.add(resource)
+    return result
+
+# Used to get objects out of sets
+class _Grab:
+    def __init__(self, value):
+        self.search_value = value
+    def __hash__(self):
+        return hash(self.search_value)
+    def __eq__(self, other):
+        if self.search_value == other:
+            self.actual_value = other
+            return True
+        return False
+
+def add_resource_to_set(resourceset, resource):
+    grabber = _Grab(resource)
+    if grabber in resourceset:
+        grabber.actual_value.locations.extend(resource.locations)
+    else:
+        resourceset.update([resource])
+
+def merge_resources(set1, set2):
+    for resource in set2:
+        add_resource_to_set(set1, resource)
diff --git a/car-ui-lib/tests/apitest/verify_rro.py b/car-ui-lib/tests/apitest/verify_rro.py
new file mode 100755
index 0000000..a58210d
--- /dev/null
+++ b/car-ui-lib/tests/apitest/verify_rro.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+#
+# 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.
+
+import argparse
+import sys
+from resource_utils import get_all_resources, remove_layout_resources, merge_resources
+from git_utils import has_chassis_changes
+
+def main():
+    parser = argparse.ArgumentParser(description="Check that an rro does not attempt to overlay any resources that don't exist")
+    parser.add_argument('--sha', help='Git hash of current changes. This script will not run if this is provided and there are no chassis changes.')
+    parser.add_argument('-r', '--rro', action='append', nargs=1, help='res folder of an RRO')
+    parser.add_argument('-b', '--base', action='append', nargs=1, help='res folder of what is being RROd')
+    args = parser.parse_args()
+
+    if not has_chassis_changes(args.sha):
+        # Don't run because there were no chassis changes
+        return
+
+    if args.rro is None or args.base is None:
+        parser.print_help()
+        sys.exit(1)
+
+    rro_resources = set()
+    for resDir in args.rro:
+        merge_resources(rro_resources, remove_layout_resources(get_all_resources(resDir[0])))
+
+    base_resources = set()
+    for resDir in args.base:
+        merge_resources(base_resources, remove_layout_resources(get_all_resources(resDir[0])))
+
+    extras = rro_resources.difference(base_resources)
+    if len(extras) > 0:
+        print("RRO attempting to override resources that don't exist:\n"
+              + '\n'.join(map(lambda x: str(x), extras)))
+        sys.exit(1)
+
+if __name__ == "__main__":
+    main()
diff --git a/car-ui-lib/tests/paintbooth/Android.mk b/car-ui-lib/tests/paintbooth/Android.mk
new file mode 100644
index 0000000..fca93f1
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/Android.mk
@@ -0,0 +1,49 @@
+#
+# 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.
+#
+
+ifneq ($(TARGET_BUILD_PDK), true)
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := PaintBooth
+
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_CERTIFICATE := platform
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+        car-ui-lib
+
+LOCAL_STATIC_JAVA_LIBRARIES += \
+    android.car.userlib \
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
+
+endif
diff --git a/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml b/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml
new file mode 100644
index 0000000..61ca559
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/AndroidManifest-gradle.xml
@@ -0,0 +1,77 @@
+<?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"
+    package="com.android.car.ui.paintbooth">
+
+  <application
+      android:icon="@drawable/ic_launcher"
+      android:label="@string/app_name"
+      android:theme="@style/Theme.CarUi.WithToolbar">
+    <activity
+        android:name=".MainActivity"
+        android:exported="true">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+        <category android:name="android.intent.category.LAUNCHER"/>
+      </intent-filter>
+    </activity>
+
+    <activity
+        android:name=".dialogs.DialogsActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".caruirecyclerview.CarUiRecyclerViewActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".caruirecyclerview.GridCarUiRecyclerViewActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".preferences.PreferenceActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".toolbar.ToolbarActivity"
+        android:exported="false"
+        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
new file mode 100644
index 0000000..ba222d2
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/AndroidManifest.xml
@@ -0,0 +1,86 @@
+<?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"
+    package="com.android.car.ui.paintbooth">
+
+  <uses-sdk
+      android:minSdkVersion="28"
+      android:targetSdkVersion="28"/>
+
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+  <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
+  <uses-permission android:name="android.permission.CHANGE_OVERLAY_PACKAGES"/>
+  <uses-permission android:name="android.permission.MANAGE_USERS"/>
+
+  <application
+      android:icon="@drawable/ic_launcher"
+      android:label="@string/app_name"
+      android:theme="@style/Theme.CarUi.WithToolbar">
+    <activity
+        android:name=".MainActivity"
+        android:exported="true">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+        <category android:name="android.intent.category.LAUNCHER"/>
+      </intent-filter>
+    </activity>
+
+    <activity
+        android:name=".dialogs.DialogsActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".caruirecyclerview.CarUiRecyclerViewActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".caruirecyclerview.GridCarUiRecyclerViewActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".preferences.PreferenceActivity"
+        android:exported="false"
+        android:parentActivityName=".MainActivity"/>
+    <activity
+        android:name=".toolbar.ToolbarActivity"
+        android:exported="false"
+        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/build.gradle b/car-ui-lib/tests/paintbooth/build.gradle
new file mode 100644
index 0000000..0ef3514
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 28
+    defaultConfig {
+        applicationId "com.android.car.ui.paintbooth"
+        minSdkVersion 28
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    sourceSets {
+        main {
+            manifest.srcFile 'AndroidManifest-gradle.xml'
+            java {
+                srcDirs = ['src']
+                filter.excludes = ["com/android/car/ui/paintbooth/overlays/OverlayManagerImpl.java"]
+            }
+            res.srcDirs = ['res']
+        }
+    }
+}
+
+dependencies {
+    implementation project(':')
+    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0'
+    api 'androidx.annotation:annotation:1.1.0'
+    api 'androidx.constraintlayout:constraintlayout:1.1.3'
+    api 'androidx.recyclerview:recyclerview:1.0.0'
+}
diff --git a/car-ui-lib/tests/paintbooth/gradlew b/car-ui-lib/tests/paintbooth/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/car-ui-lib/tests/paintbooth/gradlew.bat b/car-ui-lib/tests/paintbooth/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windows variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/car-ui-lib/tests/paintbooth/res/drawable/ic_cut.xml b/car-ui-lib/tests/paintbooth/res/drawable/ic_cut.xml
new file mode 100644
index 0000000..aff7002
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/drawable/ic_cut.xml
@@ -0,0 +1,28 @@
+<?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"
+        android:width="500dp"
+        android:height="500dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24"
+        android:tint="#FF0000">
+    <path
+        android:pathData="M0 0h48v48H0z"/>
+    <path
+        android:fillColor="#000000"
+        android:pathData="M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3v-1L9.64 7.64zM6 8c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm0 12c-1.1 0-2-.89-2-2s.9-2 2-2 2 .89 2 2-.9 2-2 2zm6-7.5c-.28 0-.5-.22-.5-.5s.22-.5.5-.5.5.22.5.5-.22.5-.5.5zM19 3l-6 6 2 2 7-7V3z"/>
+</vector>
diff --git a/car-ui-lib/tests/paintbooth/res/drawable/ic_launcher.png b/car-ui-lib/tests/paintbooth/res/drawable/ic_launcher.png
new file mode 100644
index 0000000..2af53a4
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/drawable/ic_launcher.png
Binary files differ
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/drawable/ic_settings_wifi.xml b/car-ui-lib/tests/paintbooth/res/drawable/ic_settings_wifi.xml
new file mode 100644
index 0000000..9a09d70
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/drawable/ic_settings_wifi.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ 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"
+    android:width="64dp"
+    android:height="64dp"
+    android:viewportWidth="64"
+    android:viewportHeight="64">
+  <path
+      android:pathData="M36.87,39.05a7,7 0,1 1,-9.901 9.9,7 7,0 0,1 9.901,-9.9zM14.243,26.323c9.763,-9.764 25.593,-9.764 35.355,0M53.84,22.08C41.735,9.972 22.106,9.972 10,22.08M18.486,30.565c7.42,-7.42 19.449,-7.42 26.869,0M22.728,34.808c5.077,-5.077 13.308,-5.077 18.385,0"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:fillType="evenOdd"
+      android:strokeColor="#FFF"/>
+</vector>
diff --git a/car-media-common/res/drawable/ic_media_select_arrow_drop_down.xml b/car-ui-lib/tests/paintbooth/res/drawable/ic_tracklist.xml
similarity index 60%
copy from car-media-common/res/drawable/ic_media_select_arrow_drop_down.xml
copy to car-ui-lib/tests/paintbooth/res/drawable/ic_tracklist.xml
index 2774d0f..69b5a3e 100644
--- a/car-media-common/res/drawable/ic_media_select_arrow_drop_down.xml
+++ b/car-ui-lib/tests/paintbooth/res/drawable/ic_tracklist.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  Copyright 2018, 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.
@@ -15,11 +15,15 @@
   limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
+        android:width="500dp"
+        android:height="500dp"
+        android:viewportWidth="48"
+        android:viewportHeight="48">
     <path
-        android:fillColor="#FF000000"
-        android:pathData="M7,10l5,5 5,-5z"/>
+        android:pathData="M0 0h48v48H0z"/>
+    <path
+        android:fillColor="#FF0000"
+        android:pathData="M30 12H6v4h24v-4zm0 8H6v4h24v-4zM6
+32h16v-4H6v4zm28-20v16.37c-.63-.23-1.29-.37-2-.37-3.31 0-6 2.69-6 6s2.69 6 6 6
+6-2.69 6-6V16h6v-4H34z"/>
 </vector>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_activity.xml
similarity index 72%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_activity.xml
index c5d298b..15b6fe6 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_activity.xml
@@ -14,12 +14,9 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
+<com.android.car.ui.recyclerview.CarUiRecyclerView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+    android:id="@+id/list"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/car_ui_activity_background"/>
\ No newline at end of file
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_list_item.xml
similarity index 63%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_list_item.xml
index c5d298b..a9d45ad 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/tests/paintbooth/res/layout/car_ui_recycler_view_list_item.xml
@@ -14,12 +14,18 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal">
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:layout_marginBottom="10dp"
+        android:id="@+id/textTitle"
+        android:text="TESTING"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/tests/paintbooth/res/layout/grid_car_ui_recycler_view_activity.xml
similarity index 68%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/tests/paintbooth/res/layout/grid_car_ui_recycler_view_activity.xml
index c5d298b..5c97fde 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/tests/paintbooth/res/layout/grid_car_ui_recycler_view_activity.xml
@@ -14,12 +14,12 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<FrameLayout
+<com.android.car.ui.recyclerview.CarUiRecyclerView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+    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:background="@drawable/car_ui_activity_background" />
\ No newline at end of file
diff --git a/car-ui-lib/tests/paintbooth/res/layout/list_item.xml b/car-ui-lib/tests/paintbooth/res/layout/list_item.xml
new file mode 100644
index 0000000..f60908c
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/layout/list_item.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+    <Button
+        android:id="@+id/button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:layout_marginTop="20dp"
+        android:text="Button"/>
+</RelativeLayout>
diff --git a/car-ui-lib/tests/paintbooth/res/layout/widgets_activity.xml b/car-ui-lib/tests/paintbooth/res/layout/widgets_activity.xml
new file mode 100644
index 0000000..dcadcea
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/layout/widgets_activity.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+
+    <CheckBox
+        android:id="@+id/check"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/widget_checkbox_text"/>
+
+    <Switch
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/widget_switch_text"/>
+</LinearLayout>
diff --git a/car-ui-lib/tests/paintbooth/res/values/arrays.xml b/car-ui-lib/tests/paintbooth/res/values/arrays.xml
new file mode 100644
index 0000000..6d4c2c4
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/values/arrays.xml
@@ -0,0 +1,38 @@
+<?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.
+  -->
+<resources>
+
+  <!-- Used for the entries in a list preference [CHAR_LIMIT=NONE]-->
+  <string-array name="entries">
+    <!-- The first sample choice [CHAR_LIMIT=NONE]-->
+    <item>Choose me!</item>
+    <!-- The second sample choice [CHAR_LIMIT=NONE]-->
+    <item>No, me!</item>
+    <!-- The third sample choice [CHAR_LIMIT=NONE]-->
+    <item>What about me?!</item>
+  </string-array>
+
+  <!-- Used for the values in a list preference -->
+  <string-array name="entry_values" translatable="false">
+    <!-- The first sample value -->
+    <item>alpha</item>
+    <!-- The second sample value -->
+    <item>beta</item>
+    <!-- The third sample value -->
+    <item>charlie</item>
+  </string-array>
+</resources>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/tests/paintbooth/res/values/colors.xml
similarity index 65%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/tests/paintbooth/res/values/colors.xml
index c5d298b..6e93be6 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/tests/paintbooth/res/values/colors.xml
@@ -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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+<resources>
+  <color name="dialog_activity_background_color">#ffff0c</color>
+</resources>
diff --git a/car-ui-lib/tests/paintbooth/res/values/strings.xml b/car-ui-lib/tests/paintbooth/res/values/strings.xml
new file mode 100644
index 0000000..cd45f77
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/values/strings.xml
@@ -0,0 +1,261 @@
+<?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.
+  -->
+<resources>
+  <!-- Application name [CHAR LIMIT=30] -->
+  <string name="app_name" translatable="false">Paint Booth (Gerrit)</string>
+
+  <!-- Strings for Preference Samples -->
+  <eat-comment/>
+
+  <!-- Preferences page title [CHAR_LIMIT=13] -->
+  <string name="preferences_screen_title">Settings</string>
+
+  <!--This section is for basic attributes -->
+  <eat-comment/>
+  <!-- Category title for basic preferences [CHAR_LIMIT=26]-->
+  <string name="basic_preferences">Basic attributes</string>
+  <!-- Title of a basic preference [CHAR_LIMIT=10]-->
+  <string name="title_basic_preference">Preference</string>
+  <!-- Summary of a basic preference [CHAR_LIMIT=71]-->
+  <string name="summary_basic_preference">Simple preference with no special attributes</string>
+
+  <!-- Title of a preference with stylized text [CHAR_LIMIT=71]-->
+  <string name="title_stylish_preference"><b>Very</b> <i>stylish</i> <u>preference</u></string>
+  <!-- Summary of a preference with stylized text [CHAR_LIMIT=150]-->
+  <string name="summary_stylish_preference">Define style tags such as &lt;b&gt; in a string resource to customize a preference\'s text</string>
+
+  <!-- Title of an icon preference [CHAR_LIMIT=25]-->
+  <string name="title_icon_preference">Icon preference</string>
+  <!-- Summary of an icon preference [CHAR_LIMIT=103]-->
+  <string name="summary_icon_preference">Define a drawable to display it at the start of the preference</string>
+
+  <!-- Title of a single line title preference [CHAR_LIMIT=165]-->
+  <string name="title_single_line_title_preference">Single line title preference - no matter how long the title is it will never wrap to multiple lines</string>
+  <!-- Summary of a single line title preference [CHAR_LIMIT=108]-->
+  <string name="summary_single_line_title_preference">This title will be ellipsized instead of wrapping to another line</string>
+
+  <!-- Title of a single line title preference without summary [CHAR_LIMIT=28]-->
+  <string name="title_single_line_no_summary">Single line preference - no summary</string>
+
+  <!--This section is for preferences that contain a widget in their layout -->
+  <eat-comment/>
+  <!-- Category title for preferences with widgets [CHAR_LIMIT=12]-->
+  <string name="widgets">Widgets</string>
+
+  <!-- Title of a checkbox preference [CHAR_LIMIT=31]-->
+  <string name="title_checkbox_preference">Checkbox preference</string>
+  <!-- Summary of a checkbox preference [CHAR_LIMIT=78]-->
+  <string name="summary_checkbox_preference">Tap anywhere in this preference to toggle state</string>
+
+  <!-- Title of a switch preference [CHAR_LIMIT=28]-->
+  <string name="title_switch_preference">Switch preference</string>
+  <!-- Summary of a switch preference [CHAR_LIMIT=78]-->
+  <string name="summary_switch_preference">Tap anywhere in this preference to toggle state</string>
+
+  <!-- Title of a dropdown preference [CHAR_LIMIT=31]-->
+  <string name="title_dropdown_preference">Dropdown preference</string>
+
+  <!-- Title of a seekbar preference [CHAR_LIMIT=30]-->
+  <string name="title_seekbar_preference">Seekbar preference</string>
+
+  <!--This section is for preferences that launch a dialog to edit the preference -->
+  <eat-comment/>
+  <!-- Category title for preferences which launch dialogs [CHAR_LIMIT=12]-->
+  <string name="dialogs">Dialogs</string>
+
+  <!-- Title of an edittext preference [CHAR_LIMIT=32]-->
+  <string name="title_edittext_preference">EditText preference</string>
+  <!-- Title of the dialog for an edittext preference [CHAR_LIMIT=43]-->
+  <string name="dialog_title_edittext_preference">This title can be changed!</string>
+
+  <!-- Title of a list preference [CHAR_LIMIT=25]-->
+  <string name="title_list_preference">List preference</string>
+  <!-- Title of the dialog for a list preference [CHAR_LIMIT=30]-->
+  <string name="dialog_title_list_preference">Choose one option!</string>
+
+  <!-- Title of a multi-select list preference [CHAR_LIMIT=46]-->
+  <string name="title_multi_list_preference">Multi-select list preference</string>
+  <!-- Summary of a multi-select list preference [CHAR_LIMIT=71]-->
+  <string name="summary_multi_list_preference">Shows a dialog with multiple choice options</string>
+  <!-- Title of the dialog for a multi-select list preference [CHAR_LIMIT=33]-->
+  <string name="dialog_title_multi_list_preference">Choose some options!</string>
+
+  <!--This section is for advanced attributes-->
+  <eat-comment/>
+  <!-- Category title for preferences with advanced attributes [CHAR_LIMIT=32]-->
+  <string name="advanced_attributes">Advanced attributes</string>
+
+  <!-- Title of an expandable preference [CHAR_LIMIT=45]-->
+  <string name="title_expandable_preference">Expandable preference group</string>
+  <!-- Summary of an expandable preference [CHAR_LIMIT=131]-->
+  <string name="summary_expandable_preference">This group shows one item and collapses the rest into the advanced button below</string>
+
+  <!-- Title of a preference which launches an intent [CHAR_LIMIT=28]-->
+  <string name="title_intent_preference">Intent preference</string>
+  <!-- Summary of a preference which launches an intent [CHAR_LIMIT=51]-->
+  <string name="summary_intent_preference">Launches an intent when pressed</string>
+
+  <!-- Title of a parent preference [CHAR_LIMIT=28]-->
+  <string name="title_parent_preference">Parent preference</string>
+  <!-- Summary of a parent preference [CHAR_LIMIT=130]-->
+  <string name="summary_parent_preference">Toggling this preference will change the enabled state of the preference below</string>
+
+  <!-- Title of a child preference [CHAR_LIMIT=26]-->
+  <string name="title_child_preference">Child preference</string>
+  <!-- Summary of a child preference [CHAR_LIMIT=123]-->
+  <string name="summary_child_preference">The enabled state of this preference is controlled by the preference above</string>
+
+  <!-- Title of a switch preference with variable summaries [CHAR_LIMIT=45]-->
+  <string name="title_toggle_summary_preference">Variable summary preference</string>
+  <!-- Summary of a variable summary preference when the preference is on [CHAR_LIMIT=118]-->
+  <string name="summary_on_toggle_summary_preference">On! :) - the summary of this preference changes depending on its state</string>
+  <!-- Summary of a variable summary preference when the preference is off [CHAR_LIMIT=118]-->
+  <string name="summary_off_toggle_summary_preference">Off! :( - the summary of this preference changes depending on its state</string>
+
+  <!-- Title of a copyable preference [CHAR_LIMIT=31]-->
+  <string name="title_copyable_preference">Copyable preference</string>
+  <!-- Summary of a copyable preference [CHAR_LIMIT=81]-->
+  <string name="summary_copyable_preference">Long press on this preference to copy its summary</string>
+
+  <!-- Title of a Advanced preference [CHAR_LIMIT=13]-->
+  <string name="advanced_preference">Advanced</string>
+
+  <!-- Title of a Intent preference [CHAR_LIMIT=28]-->
+  <string name="intent_preference">Intent preference</string>
+
+  <!--This section is for toolbar attributes -->
+  <eat-comment/>
+
+  <!-- Text for change title button [CHAR_LIMIT=20]-->
+  <string name="toolbar_change_title">Change title</string>
+
+  <!-- Text for set xml button [CHAR_LIMIT=45]-->
+  <string name="toolbar_set_xml_resource">MenuItem: Set to XML source</string>
+
+  <!-- Text for add icon button [CHAR_LIMIT=30]-->
+  <string name="toolbar_add_icon">MenuItem: Add Icon</string>
+
+  <!-- Text for add untined icon button [CHAR_LIMIT=45]-->
+  <string name="toolbar_add_untined_icon">MenuItem: Add untinted icon</string>
+
+  <!-- Text for add overflow button [CHAR_LIMIT=36]-->
+  <string name="toolbar_add_overflow">MenuItem: Add Overflow</string>
+
+  <!-- Text for add switch button [CHAR_LIMIT=33]-->
+  <string name="toolbar_add_switch">MenuItem: Add Switch</string>
+
+  <!-- Text for add text button [CHAR_LIMIT=30]-->
+  <string name="toolbar_add_text">MenuItem: Add text</string>
+
+  <!-- Text for add icon text button [CHAR_LIMIT=45]-->
+  <string name="toolbar_add_icon_text">MenuItem: Add icon and text</string>
+
+  <!-- Text for add untined icon and text button [CHAR_LIMIT=60]-->
+  <string name="toolbar_add_untinted_icon_and_text">MenuItem: Add untinted icon and text</string>
+
+  <!-- Text for add activatable button [CHAR_LIMIT=41]-->
+  <string name="toolbar_add_activatable">MenuItem: Add activatable</string>
+
+  <!-- Text for add activatable button [CHAR_LIMIT=36]-->
+  <string name="toolbar_add_morphing">MenuItem: Add morphing</string>
+
+  <!-- Text for toggle visibility button [CHAR_LIMIT=45]-->
+  <string name="toolbar_toggle_visibility">MenuItem: Toggle Visibility</string>
+
+  <!-- Text for toggle enable button [CHAR_LIMIT=40]-->
+  <string name="toolbar_toggle_enable">MenuItem: Toggle Enabled</string>
+
+  <!-- Text for toggle enable button [CHAR_LIMIT=49]-->
+  <string name="toolbar_toggle_perform_click">MenuItem: Call PerformClick()</string>
+
+  <!-- Text for toggle icon button [CHAR_LIMIT=35]-->
+  <string name="toolbar_toggle_icon">MenuItem: Toggle Icon</string>
+
+  <!-- Text for toggle show while search button [CHAR_LIMIT=61]-->
+  <string name="toolbar_toggle_show_while_search">MenuItem: Toggle show while searching</string>
+
+  <!-- Text for cycle nav button mode button [CHAR_LIMIT=35]-->
+  <string name="toolbar_cycle_nav_button">Cycle nav button mode</string>
+
+  <!-- Text for toggle logo button [CHAR_LIMIT=19]-->
+  <string name="toolbar_toggle_logo">Toggle logo</string>
+
+  <!-- Text for cycle state button [CHAR_LIMIT=20]-->
+  <string name="toolbar_cycle_state">Cycle state</string>
+
+  <!-- Text for toggle search hint button [CHAR_LIMIT=30]-->
+  <string name="toolbar_toggle_search_hint">Toggle search hint</string>
+
+  <!-- Text for toggle background button [CHAR_LIMIT=30]-->
+  <string name="toolbar_toggle_background">Toggle background</string>
+
+  <!-- Text for add tab button [CHAR_LIMIT=12]-->
+  <string name="toolbar_add_tab">Add tab</string>
+
+  <!-- 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>
+
+  <!--This section is for dialog attributes -->
+  <eat-comment/>
+
+  <!-- Text for show dialog button [CHAR_LIMIT=18]-->
+  <string name="dialog_show_dialog">Show Dialog</string>
+
+  <!-- Text for show dialog button [CHAR_LIMIT=30]-->
+  <string name="dialog_show_dialog_icon">Show Dialog with icon</string>
+
+  <!-- Text for Dialog with edit text box button [CHAR_LIMIT=50]-->
+  <string name="dialog_show_dialog_edit">Show Dialog with edit text box</string>
+
+  <!-- Text for show Dialog with only positive button button [CHAR_LIMIT=61]-->
+  <string name="dialog_show_dialog_only_positive">Show Dialog with only positive button</string>
+
+  <!-- Text for show Dialog With no button provided button [CHAR_LIMIT=60]-->
+  <string name="dialog_show_dialog_no_button">Show Dialog With no button provided</string>
+
+  <!-- Text for show Dialog With Checkbox button [CHAR_LIMIT=41]-->
+  <string name="dialog_show_dialog_checkbox">Show Dialog With Checkbox</string>
+
+  <!-- Text for show Dialog without title button [CHAR_LIMIT=41]-->
+  <string name="dialog_show_dialog_no_title">Show Dialog without title</string>
+
+  <!-- Text for show Toast button [CHAR_LIMIT=16]-->
+  <string name="dialog_show_toast">Show Toast</string>
+
+  <!-- Button that shows a dialog with a subtitle [CHAR_LIMIT=50]-->
+  <string name="dialog_show_subtitle">Show Dialog with title and subtitle</string>
+
+  <!-- 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]-->
+  <string name="widget_checkbox_text">I\'m a check box</string>
+  <!-- Text for switch [CHAR_LIMIT=25]-->
+  <string name="widget_switch_text">I\'m a switch</string>
+  
+</resources>
diff --git a/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml b/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml
new file mode 100644
index 0000000..4f22379
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml
@@ -0,0 +1,32 @@
+<?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.
+  -->
+<MenuItems xmlns:app="http://schemas.android.com/apk/res-auto">
+    <MenuItem app:search="true"/>
+    <MenuItem app:settings="true"/>
+    <MenuItem
+        app:title="@string/preferences_screen_title"/>
+    <MenuItem
+        app:id="@+id/menu_bar"
+        app:title="Bar"
+        app:icon="@drawable/ic_tracklist"
+        app:onClick="xmlMenuItemClicked"/>
+    <MenuItem
+        app:title="Bar"
+        app:checkable="true"
+        app:uxRestrictions="UX_RESTRICTIONS_FULLY_RESTRICTED"
+        app:onClick="xmlMenuItemClicked"/>
+</MenuItems>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/tests/paintbooth/res/xml/preference_overlays.xml
similarity index 65%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/tests/paintbooth/res/xml/preference_overlays.xml
index c5d298b..27ef049 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/tests/paintbooth/res/xml/preference_overlays.xml
@@ -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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+<PreferenceScreen
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:title="Overlays">
+</PreferenceScreen>
\ No newline at end of file
diff --git a/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml b/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml
new file mode 100644
index 0000000..b49dd0d
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/xml/preference_samples.xml
@@ -0,0 +1,150 @@
+<?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.
+  -->
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    app:title="@string/preferences_screen_title">
+
+  <PreferenceCategory
+      android:title="@string/basic_preferences">
+
+    <Preference
+        android:key="preference"
+        android:title="@string/title_basic_preference"
+        android:summary="@string/summary_basic_preference"/>
+
+    <Preference
+        android:key="stylized"
+        android:title="@string/title_stylish_preference"
+        android:summary="@string/summary_stylish_preference"/>
+
+    <Preference
+        android:key="icon"
+        android:title="@string/title_icon_preference"
+        android:summary="@string/summary_icon_preference"
+        android:icon="@drawable/ic_settings_wifi"/>
+
+    <Preference
+        android:key="single_line_title"
+        android:title="@string/title_single_line_title_preference"
+        android:summary="@string/summary_single_line_title_preference"
+        app:singleLineTitle="true"/>
+
+    <Preference
+        android:key="single_line_no_summary"
+        android:title="@string/title_single_line_no_summary"
+        app:singleLineTitle="true"/>
+  </PreferenceCategory>
+
+  <PreferenceCategory
+      android:title="@string/widgets">
+
+    <CheckBoxPreference
+        android:key="checkbox"
+        android:title="@string/title_checkbox_preference"
+        android:summary="@string/summary_checkbox_preference"/>
+
+    <SwitchPreference
+        android:key="switch"
+        android:title="@string/title_switch_preference"
+        android:summary="@string/summary_switch_preference"/>
+
+    <DropDownPreference
+        android:key="dropdown"
+        android:title="@string/title_dropdown_preference"
+        android:entries="@array/entries"
+        app:useSimpleSummaryProvider="true"
+        android:entryValues="@array/entry_values"/>
+
+    <SeekBarPreference
+        android:key="seekbar"
+        android:title="@string/title_seekbar_preference"
+        android:max="10"
+        android:defaultValue="5"/>
+  </PreferenceCategory>
+
+  <PreferenceCategory
+      android:title="@string/dialogs">
+
+    <EditTextPreference
+        android:key="edittext"
+        android:title="@string/title_edittext_preference"
+        app:useSimpleSummaryProvider="true"
+        android:dialogTitle="@string/dialog_title_edittext_preference"/>
+
+    <ListPreference
+        android:key="list"
+        android:title="@string/title_list_preference"
+        app:useSimpleSummaryProvider="true"
+        android:entries="@array/entries"
+        android:entryValues="@array/entry_values"
+        android:dialogTitle="@string/dialog_title_list_preference"/>
+
+    <MultiSelectListPreference
+        android:key="multi_select_list"
+        android:title="@string/title_multi_list_preference"
+        android:summary="@string/summary_multi_list_preference"
+        android:entries="@array/entries"
+        android:entryValues="@array/entry_values"
+        android:dialogTitle="@string/dialog_title_multi_list_preference"/>
+  </PreferenceCategory>
+
+  <PreferenceCategory
+      android:key="@string/advanced_preference"
+      android:title="@string/advanced_attributes"
+      app:initialExpandedChildrenCount="1">
+
+    <Preference
+        android:key="expandable"
+        android:title="@string/title_expandable_preference"
+        android:summary="@string/summary_expandable_preference"/>
+
+    <Preference
+        android:title="@string/title_intent_preference"
+        android:summary="@string/summary_intent_preference">
+
+      <intent android:action="android.intent.action.VIEW"
+          android:data="http://www.android.com"/>
+
+    </Preference>
+
+    <SwitchPreference
+        android:key="parent"
+        android:title="@string/title_parent_preference"
+        android:summary="@string/summary_parent_preference"/>
+
+    <SwitchPreference
+        android:key="child"
+        android:dependency="parent"
+        android:title="@string/title_child_preference"
+        android:summary="@string/summary_child_preference"/>
+
+    <SwitchPreference
+        android:key="toggle_summary"
+        android:title="@string/title_toggle_summary_preference"
+        android:summaryOn="@string/summary_on_toggle_summary_preference"
+        android:summaryOff="@string/summary_off_toggle_summary_preference"/>
+
+    <Preference
+        android:key="copyable"
+        android:title="@string/title_copyable_preference"
+        android:summary="@string/summary_copyable_preference"
+        android:selectable="false"
+        app:enableCopying="true"/>
+  </PreferenceCategory>
+
+</PreferenceScreen>
\ No newline at end of file
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
new file mode 100644
index 0000000..3cf2b9f
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/MainActivity.java
@@ -0,0 +1,208 @@
+/*
+ * 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.paintbooth;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.Toast;
+
+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;
+import com.android.car.ui.paintbooth.dialogs.DialogsActivity;
+import com.android.car.ui.paintbooth.overlays.OverlayActivity;
+import com.android.car.ui.paintbooth.preferences.PreferenceActivity;
+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;
+import java.util.List;
+
+/**
+ * Paint booth app
+ */
+public class MainActivity extends Activity implements InsetsChangedListener {
+
+    /**
+     * List of all sample activities.
+     */
+    private final List<Pair<String, Class<? extends Activity>>> mActivities = Arrays.asList(
+            Pair.create("Dialogs sample", DialogsActivity.class),
+            Pair.create("List sample", CarUiRecyclerViewActivity.class),
+            Pair.create("Grid sample", GridCarUiRecyclerViewActivity.class),
+            Pair.create("Preferences sample", PreferenceActivity.class),
+            Pair.create("Overlays", OverlayActivity.class),
+            Pair.create("Toolbar sample", ToolbarActivity.class),
+            Pair.create("Widget sample", WidgetActivity.class),
+            Pair.create("ListItem sample", CarUiListItemActivity.class)
+    );
+
+    private class ViewHolder extends RecyclerView.ViewHolder {
+        private Button mButton;
+
+        ViewHolder(@NonNull View itemView) {
+            super(itemView);
+            mButton = itemView.findViewById(R.id.button);
+        }
+
+        void update(String title, Class<? extends Activity> activityClass) {
+            mButton.setText(title);
+            mButton.setOnClickListener(e -> {
+                Intent intent = new Intent(mButton.getContext(), activityClass);
+                startActivity(intent);
+            });
+        }
+    }
+
+    private final RecyclerView.Adapter<ViewHolder> mAdapter =
+            new RecyclerView.Adapter<ViewHolder>() {
+        @NonNull
+        @Override
+        public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            View item = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent,
+                    false);
+            return new ViewHolder(item);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+            Pair<String, Class<? extends Activity>> item = mActivities.get(position);
+            holder.update(item.first, item.second);
+        }
+
+        @Override
+        public int getItemCount() {
+            return mActivities.size();
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.car_ui_recycler_view_activity);
+
+        ToolbarController toolbar = CarUi.requireToolbar(this);
+        toolbar.setLogo(R.drawable.ic_launcher);
+        toolbar.setTitle(getTitle());
+
+        CarUiRecyclerView prv = findViewById(R.id.list);
+        prv.setAdapter(mAdapter);
+
+        initLeakCanary();
+    }
+
+    private void initLeakCanary() {
+        // This sets LeakCanary to report errors after a single leak instead of 5, and to ask for
+        // permission to use storage, which it needs to work.
+        //
+        // Equivalent to this non-reflection code:
+        //
+        // Config config = LeakCanary.INSTANCE.getConfig();
+        // LeakCanary.INSTANCE.setConfig(config.copy(config.getDumpHeap(),
+        //     config.getDumpHeapWhenDebugging(),
+        //     1,
+        //     config.getReferenceMatchers(),
+        //     config.getObjectInspectors(),
+        //     config.getOnHeapAnalyzedListener(),
+        //     config.getMetatadaExtractor(),
+        //     config.getComputeRetainedHeapSize(),
+        //     config.getMaxStoredHeapDumps(),
+        //     true,
+        //     config.getUseExperimentalLeakFinders()));
+        try {
+            Class<?> canaryClass = Class.forName("leakcanary.LeakCanary");
+            try {
+                Class<?> onHeapAnalyzedListenerClass =
+                        Class.forName("leakcanary.OnHeapAnalyzedListener");
+                Class<?> metadataExtractorClass = Class.forName("shark.MetadataExtractor");
+                Method getConfig = canaryClass.getMethod("getConfig");
+                Class<?> configClass = getConfig.getReturnType();
+                Method setConfig = canaryClass.getMethod("setConfig", configClass);
+                Method copy = configClass.getMethod("copy", boolean.class, boolean.class,
+                        int.class, List.class, List.class, onHeapAnalyzedListenerClass,
+                        metadataExtractorClass, boolean.class, int.class, boolean.class,
+                        boolean.class);
+
+                Object canary = canaryClass.getField("INSTANCE").get(null);
+                Object currentConfig = getConfig.invoke(canary);
+
+                Boolean dumpHeap = (Boolean) configClass
+                        .getMethod("getDumpHeap").invoke(currentConfig);
+                Boolean dumpHeapWhenDebugging = (Boolean) configClass
+                        .getMethod("getDumpHeapWhenDebugging").invoke(currentConfig);
+                List<?> referenceMatchers = (List<?>) configClass
+                        .getMethod("getReferenceMatchers").invoke(currentConfig);
+                List<?> objectInspectors = (List<?>) configClass
+                        .getMethod("getObjectInspectors").invoke(currentConfig);
+                Object onHeapAnalyzedListener = configClass
+                        .getMethod("getOnHeapAnalyzedListener").invoke(currentConfig);
+                // Yes, LeakCanary misspelled metadata
+                Object metadataExtractor = configClass
+                        .getMethod("getMetatadaExtractor").invoke(currentConfig);
+                Boolean computeRetainedHeapSize = (Boolean) configClass
+                        .getMethod("getComputeRetainedHeapSize").invoke(currentConfig);
+                Integer maxStoredHeapDumps = (Integer) configClass
+                        .getMethod("getMaxStoredHeapDumps").invoke(currentConfig);
+                Boolean useExperimentalLeakFinders = (Boolean) configClass
+                        .getMethod("getUseExperimentalLeakFinders").invoke(currentConfig);
+
+                setConfig.invoke(canary, copy.invoke(currentConfig,
+                        dumpHeap,
+                        dumpHeapWhenDebugging,
+                        1,
+                        referenceMatchers,
+                        objectInspectors,
+                        onHeapAnalyzedListener,
+                        metadataExtractor,
+                        computeRetainedHeapSize,
+                        maxStoredHeapDumps,
+                        true,
+                        useExperimentalLeakFinders));
+
+            } catch (ReflectiveOperationException e) {
+                Log.e("paintbooth", "Error initializing LeakCanary", e);
+                Toast.makeText(this, "Error initializing LeakCanary", Toast.LENGTH_LONG).show();
+            }
+        } catch (ClassNotFoundException e) {
+            // 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
new file mode 100644
index 0000000..927b3f4
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
@@ -0,0 +1,208 @@
+/*
+ * 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.paintbooth.caruirecyclerview;
+
+import android.app.Activity;
+import android.content.Context;
+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.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 implements InsetsChangedListener {
+
+    private final ArrayList<CarUiListItem> mData = new ArrayList<>();
+    private CarUiListItemAdapter mAdapter;
+
+    @Override
+    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);
+        mAdapter = new CarUiListItemAdapter(generateDummyData());
+        recyclerView.setAdapter(mAdapter);
+    }
+
+    private ArrayList<CarUiListItem> generateDummyData() {
+        Context context = this;
+
+        CarUiHeaderListItem header = new CarUiHeaderListItem("First header");
+        mData.add(header);
+
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+        item.setTitle("Test title");
+        item.setBody("Test body");
+        mData.add(item);
+
+        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(CarUiContentListItem.Action.NONE);
+        item.setBody("Test body with no title");
+        mData.add(item);
+
+        item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+        item.setTitle("Test Title");
+        item.setIcon(getDrawable(R.drawable.ic_launcher));
+        mData.add(item);
+
+        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(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.setOnCheckedChangeListener(
+                (listItem, isChecked) -> Toast.makeText(context,
+                        "Item checked state is: " + isChecked, Toast.LENGTH_SHORT).show());
+        mData.add(item);
+
+        item = new CarUiContentListItem(CarUiContentListItem.Action.CHECK_BOX);
+        item.setIcon(getDrawable(R.drawable.ic_launcher));
+        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(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.setChecked(true);
+        mData.add(item);
+
+        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.setChecked(false);
+        radioItem1.setOnCheckedChangeListener((listItem, isChecked) -> {
+            if (isChecked) {
+                radioItem2.setChecked(false);
+                mAdapter.notifyItemChanged(mData.indexOf(radioItem2));
+            }
+        });
+        mData.add(radioItem1);
+
+        radioItem2.setIcon(getDrawable(R.drawable.ic_launcher));
+        radioItem2.setTitle("Item is mutually exclusive with item above");
+        radioItem2.setChecked(true);
+        radioItem2.setOnCheckedChangeListener((listItem, isChecked) -> {
+            if (isChecked) {
+                radioItem1.setChecked(false);
+                mAdapter.notifyItemChanged(mData.indexOf(radioItem1));
+            }
+        });
+        mData.add(radioItem2);
+
+        item = new CarUiContentListItem(CarUiContentListItem.Action.ICON);
+        item.setIcon(getDrawable(R.drawable.ic_launcher));
+        item.setTitle("Title");
+        item.setBody("Random body text -- with action divider");
+        item.setActionDividerVisible(true);
+        item.setSupplementalIcon(getDrawable(R.drawable.ic_launcher));
+        item.setChecked(true);
+        mData.add(item);
+
+        item = new CarUiContentListItem(CarUiContentListItem.Action.ICON);
+        item.setIcon(getDrawable(R.drawable.ic_launcher));
+        item.setTitle("Null supplemental icon");
+        item.setChecked(true);
+        mData.add(item);
+
+        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",
+                        Toast.LENGTH_SHORT).show());
+        item.setChecked(true);
+        mData.add(item);
+
+        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
new file mode 100644
index 0000000..a3ac9c2
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiRecyclerViewActivity.java
@@ -0,0 +1,72 @@
+/*
+ * 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.paintbooth.caruirecyclerview;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+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 implements InsetsChangedListener {
+    private final ArrayList<String> mData = new ArrayList<>();
+    private final int mDataToGenerate = 100;
+
+    @Override
+    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));
+
+        RecyclerViewAdapter adapter = new RecyclerViewAdapter(generateDummyData());
+        recyclerView.setAdapter(adapter);
+    }
+
+    private ArrayList<String> generateDummyData() {
+        for (int i = 0; i <= mDataToGenerate; i++) {
+            mData.add("data" + i);
+        }
+        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
new file mode 100644
index 0000000..586b633
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/GridCarUiRecyclerViewActivity.java
@@ -0,0 +1,67 @@
+/*
+ * 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.paintbooth.caruirecyclerview;
+
+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 implements
+        InsetsChangedListener {
+    private final ArrayList<String> mData = new ArrayList<>();
+    private final int mDataToGenerate = 200;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.grid_car_ui_recycler_view_activity);
+
+        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);
+    }
+
+    private ArrayList<String> generateDummyData() {
+        for (int i = 1; i <= mDataToGenerate; i++) {
+            mData.add("data" + i);
+        }
+        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/RecyclerViewAdapter.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/RecyclerViewAdapter.java
new file mode 100644
index 0000000..652a7a8
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/RecyclerViewAdapter.java
@@ -0,0 +1,75 @@
+/*
+ * 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.paintbooth.caruirecyclerview;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.paintbooth.R;
+
+import java.util.ArrayList;
+
+/**
+ * Implementation of {@link RecyclerViewAdapter} that can be used with RecyclerViews.
+ */
+public class RecyclerViewAdapter extends
+        RecyclerView.Adapter<RecyclerViewAdapter.RecyclerViewHolder> {
+
+    private ArrayList<String> mData;
+
+    RecyclerViewAdapter(ArrayList<String> data) {
+        this.mData = data;
+    }
+
+    @NonNull
+    @Override
+    public RecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        LayoutInflater inflator = LayoutInflater.from(parent.getContext());
+        View view = inflator.inflate(R.layout.car_ui_recycler_view_list_item, parent, false);
+        return new RecyclerViewHolder(view);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) {
+        String title = mData.get(position);
+        holder.mTextTitle.setText(title);
+    }
+
+    @Override
+    public int getItemCount() {
+        return mData.size();
+    }
+
+
+    /**
+     * Holds views for each element in the list.
+     */
+    public static class RecyclerViewHolder extends RecyclerView.ViewHolder {
+        TextView mTextTitle;
+
+        RecyclerViewHolder(@NonNull View itemView) {
+            super(itemView);
+            mTextTitle = itemView.findViewById(R.id.textTitle);
+        }
+    }
+}
+
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
new file mode 100644
index 0000000..1f5f26a
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/dialogs/DialogsActivity.java
@@ -0,0 +1,243 @@
+/*
+ * 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.paintbooth.dialogs;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.Toast;
+
+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;
+
+/**
+ * Activity that shows different dialogs from the device default theme.
+ */
+public class DialogsActivity extends Activity implements InsetsChangedListener {
+
+    private final List<Pair<Integer, View.OnClickListener>> mButtons = new ArrayList<>();
+
+    @Override
+    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()));
+        mButtons.add(Pair.create(R.string.dialog_show_dialog_icon,
+                v -> showDialogWithIcon()));
+        mButtons.add(Pair.create(R.string.dialog_show_dialog_edit,
+                v -> showDialogWithTextBox()));
+        mButtons.add(Pair.create(R.string.dialog_show_dialog_only_positive,
+                v -> showDialogWithOnlyPositiveButton()));
+        mButtons.add(Pair.create(R.string.dialog_show_dialog_no_button,
+                v -> showDialogWithNoButtonProvided()));
+        mButtons.add(Pair.create(R.string.dialog_show_dialog_checkbox,
+                v -> showDialogWithCheckbox()));
+        mButtons.add(Pair.create(R.string.dialog_show_dialog_no_title,
+                v -> showDialogWithoutTitle()));
+        mButtons.add(Pair.create(R.string.dialog_show_toast,
+                v -> showToast()));
+        mButtons.add(Pair.create(R.string.dialog_show_subtitle,
+                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);
+    }
+
+    private void showDialog() {
+        new AlertDialogBuilder(this)
+                .setTitle("Standard Alert Dialog")
+                .setMessage("With a message to show.")
+                .setNeutralButton("NEUTRAL", (dialogInterface, which) -> {
+                })
+                .setPositiveButton("OK", (dialogInterface, which) -> {
+                })
+                .setNegativeButton("CANCEL", (dialogInterface, which) -> {
+                })
+                .show();
+    }
+
+    private void showDialogWithIcon() {
+        new AlertDialogBuilder(this)
+                .setTitle("Alert dialog with icon")
+                .setMessage("The message body of the alert")
+                .setIcon(R.drawable.ic_tracklist)
+                .show();
+    }
+
+    private void showDialogWithNoButtonProvided() {
+        new AlertDialogBuilder(this)
+                .setTitle("Standard Alert Dialog")
+                .show();
+    }
+
+    private void showDialogWithCheckbox() {
+        new AlertDialogBuilder(this)
+                .setTitle("Custom Dialog Box")
+                .setMultiChoiceItems(
+                        new CharSequence[]{"I am a checkbox"},
+                        new boolean[]{false},
+                        (dialog, which, isChecked) -> {
+                        })
+                .setPositiveButton("OK", (dialogInterface, which) -> {
+                })
+                .setNegativeButton("CANCEL", (dialogInterface, which) -> {
+                })
+                .show();
+    }
+
+    private void showDialogWithTextBox() {
+        new AlertDialogBuilder(this)
+                .setTitle("Standard Alert Dialog")
+                .setEditBox("Edit me please", null, null)
+                .setPositiveButton("OK", (dialogInterface, i) -> {
+                })
+                .show();
+    }
+
+    private void showDialogWithOnlyPositiveButton() {
+        new AlertDialogBuilder(this)
+                .setTitle("Standard Alert Dialog").setMessage("With a message to show.")
+                .setPositiveButton("OK", (dialogInterface, i) -> {
+                })
+                .show();
+    }
+
+    private void showDialogWithoutTitle() {
+        new AlertDialogBuilder(this)
+                .setMessage("I dont have a title.")
+                .setPositiveButton("OK", (dialogInterface, i) -> {
+                })
+                .setNegativeButton("CANCEL", (dialogInterface, which) -> {
+                })
+                .show();
+    }
+
+    private void showToast() {
+        Toast.makeText(this, "Toast message looks like this", Toast.LENGTH_LONG).show();
+    }
+
+    private void showDialogWithSubtitle() {
+        new AlertDialogBuilder(this)
+                .setTitle("My Title!")
+                .setSubtitle("My Subtitle!")
+                .setMessage("My Message!")
+                .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!")
+                .setSubtitle("My Subtitle!")
+                .setMessage("My Message!")
+                .setIcon(R.drawable.ic_tracklist)
+                .show();
+    }
+
+    private static class ViewHolder extends CarUiRecyclerView.ViewHolder {
+
+        private final Button mButton;
+
+        ViewHolder(View itemView) {
+            super(itemView);
+            mButton = itemView.requireViewById(R.id.button);
+        }
+
+        public void bind(Integer title, View.OnClickListener listener) {
+            mButton.setText(title);
+            mButton.setOnClickListener(listener);
+        }
+    }
+
+    private final CarUiRecyclerView.Adapter<ViewHolder> mAdapter =
+            new CarUiRecyclerView.Adapter<ViewHolder>() {
+                @Override
+                public int getItemCount() {
+                    return mButtons.size();
+                }
+
+                @Override
+                public ViewHolder onCreateViewHolder(ViewGroup parent, int position) {
+                    View item =
+                            LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,
+                                    parent, false);
+                    return new ViewHolder(item);
+                }
+
+                @Override
+                public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+                    Pair<Integer, View.OnClickListener> pair = mButtons.get(position);
+                    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/overlays/OverlayActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/overlays/OverlayActivity.java
new file mode 100644
index 0000000..9a49b10
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/overlays/OverlayActivity.java
@@ -0,0 +1,98 @@
+/*
+ * 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.paintbooth.overlays;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.preference.SwitchPreference;
+
+import com.android.car.ui.paintbooth.R;
+import com.android.car.ui.preference.PreferenceFragment;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Activity representing lists of RROs ppackage name as title and the corresponding target package
+ * name as the summary with a toggle switch to enable/disable the overlays.
+ */
+public class OverlayActivity extends AppCompatActivity {
+    private static final String TAG = OverlayActivity.class.getSimpleName();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Display the fragment as the main content.
+        if (savedInstanceState == null) {
+            getSupportFragmentManager()
+                    .beginTransaction()
+                    .replace(android.R.id.content, new OverlayFragment())
+                    .commit();
+        }
+    }
+
+    /** PreferenceFragmentCompat that sets the preference hierarchy from XML */
+    public static class OverlayFragment extends PreferenceFragment {
+        private OverlayManager mOverlayManager;
+
+        @Override
+        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+            try {
+                mOverlayManager = OverlayManager.getInstance(getContext());
+                setPreferencesFromResource(R.xml.preference_overlays, rootKey);
+
+                Map<String, List<OverlayManager.OverlayInfo>> overlays =
+                        OverlayManager.getInstance(getContext()).getOverlays();
+
+                for (String targetPackage : overlays.keySet()) {
+                    for (OverlayManager.OverlayInfo overlayPackage : overlays.get(targetPackage)) {
+                        SwitchPreference switchPreference = new SwitchPreference(getContext());
+                        switchPreference.setKey(overlayPackage.getPackageName());
+                        switchPreference.setTitle(overlayPackage.getPackageName());
+                        switchPreference.setSummary(targetPackage);
+                        switchPreference.setChecked(overlayPackage.isEnabled());
+
+                        switchPreference.setOnPreferenceChangeListener((preference, newValue) -> {
+                            applyOverlay(overlayPackage.getPackageName(), (boolean) newValue);
+                            return true;
+                        });
+
+                        getPreferenceScreen().addPreference(switchPreference);
+                    }
+                }
+            } catch (Exception e) {
+                Toast.makeText(getContext(), "Error: " + e.getMessage(),
+                        Toast.LENGTH_LONG).show();
+                Log.e(TAG, "Can't load overlays: ", e);
+            }
+        }
+
+        private void applyOverlay(String overlayPackage, boolean enableOverlay) {
+            try {
+                mOverlayManager.applyOverlay(overlayPackage, enableOverlay);
+            } catch (Exception e) {
+                Toast.makeText(getContext(), "Error: " + e.getMessage(),
+                        Toast.LENGTH_LONG).show();
+                Log.e(TAG, "Can't apply overlay: ", e);
+            }
+        }
+    }
+}
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/overlays/OverlayManager.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/overlays/OverlayManager.java
new file mode 100644
index 0000000..89a6c4f
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/overlays/OverlayManager.java
@@ -0,0 +1,88 @@
+/*
+ * 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.paintbooth.overlays;
+
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Abstraction around {@link IOverlayManager}, used to deal with the fact that this is a hidden
+ * API. {@link OverlayManagerImpl} should be excluded when compiling Paintbooth outside of the
+ * system image.
+ */
+public interface OverlayManager {
+    String TAG = OverlayManager.class.getSimpleName();
+
+    /** Information about a single overlay affecting a target APK */
+    interface OverlayInfo {
+        /** Name of the overlay */
+        @NonNull
+        String getPackageName();
+        /** Whether this overlay is enabled or not */
+        boolean isEnabled();
+    }
+
+    /**
+     * Returns a map of available overlays, indexed by the package name each overlay applies to
+     */
+    @NonNull
+    Map<String, List<OverlayInfo>> getOverlays() throws RemoteException;
+
+    /**
+     * Enables/disables a given overlay
+     * @param packageName an overlay package name (obtained from
+     * {@link OverlayInfo#getPackageName()})
+     */
+    void applyOverlay(@NonNull String packageName, boolean enable) throws RemoteException;
+
+    /** A null {@link OverlayManager} */
+    final class OverlayManagerStub implements OverlayManager {
+        @Override
+        @NonNull
+        public Map<String, List<OverlayInfo>> getOverlays() throws RemoteException {
+            throw new RemoteException("Overlay manager is not available");
+        }
+
+        @Override
+        public void applyOverlay(@NonNull String packageName, boolean enable)
+                throws RemoteException {
+            throw new RemoteException("Overlay manager is not available");
+        }
+    }
+
+    /** Returns a valid {@link OverlayManager} for this environment */
+    @NonNull
+    static OverlayManager getInstance(Context context) {
+        try {
+            return (OverlayManager) Class
+                    .forName("com.android.car.ui.paintbooth.overlays.OverlayManagerImpl")
+                    .getConstructor(Context.class)
+                    .newInstance(context);
+        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException
+                | NoSuchMethodException | InvocationTargetException e) {
+            Log.i(TAG, "Overlay Manager is not available");
+            return new OverlayManagerStub();
+        }
+    }
+}
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/overlays/OverlayManagerImpl.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/overlays/OverlayManagerImpl.java
new file mode 100644
index 0000000..32fcbc9
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/overlays/OverlayManagerImpl.java
@@ -0,0 +1,80 @@
+/*
+ * 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.paintbooth.overlays;
+
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Context;
+import android.content.om.IOverlayManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import androidx.annotation.NonNull;
+
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * OverlayManager implementation. Exclude this file when building Paintbooth outside of the system
+ * image.
+ */
+public class OverlayManagerImpl implements OverlayManager {
+    private final CarUserManagerHelper mCarUserManagerHelper;
+    private final IOverlayManager mOverlayManager;
+
+    public OverlayManagerImpl(Context context) {
+        mCarUserManagerHelper = new CarUserManagerHelper(context);
+        mOverlayManager = IOverlayManager.Stub.asInterface(
+                ServiceManager.getService(Context.OVERLAY_SERVICE));
+    }
+
+    private OverlayInfo convertOverlayInfo(android.content.om.OverlayInfo info) {
+        return new OverlayInfo() {
+            @NonNull
+            @Override
+            public String getPackageName() {
+                return info.packageName;
+            }
+
+            @Override
+            public boolean isEnabled() {
+                return info.state == android.content.om.OverlayInfo.STATE_ENABLED;
+            }
+        };
+    }
+
+    @Override
+    @NonNull
+    public Map<String, List<OverlayManager.OverlayInfo>> getOverlays() throws RemoteException {
+        Map<String, List<android.content.om.OverlayInfo>> overlays = mOverlayManager
+                .getAllOverlays(mCarUserManagerHelper.getCurrentForegroundUserId());
+        return overlays.entrySet()
+                .stream()
+                .collect(toMap(Map.Entry::getKey, e -> e.getValue()
+                        .stream()
+                        .map(this::convertOverlayInfo)
+                        .collect(toList())));
+    }
+
+    @Override
+    public void applyOverlay(@NonNull String packageName, boolean enable) throws RemoteException {
+        mOverlayManager.setEnabled(packageName, enable,
+                mCarUserManagerHelper.getCurrentForegroundUserId());
+    }
+}
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/preferences/PreferenceActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/preferences/PreferenceActivity.java
new file mode 100644
index 0000000..a4c9cbf
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/preferences/PreferenceActivity.java
@@ -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.
+ */
+
+package com.android.car.ui.paintbooth.preferences;
+
+import android.os.Bundle;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * Displays samples of all types of Preferences to help with verifying style changes.
+ */
+public class PreferenceActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Display the fragment as the main content.
+        if (savedInstanceState == null) {
+            getSupportFragmentManager()
+                    .beginTransaction()
+                    .replace(android.R.id.content, new PreferenceDemoFragment())
+                    .commitNow();
+        }
+    }
+}
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.java
new file mode 100644
index 0000000..ccd21ef
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/preferences/PreferenceDemoFragment.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.paintbooth.preferences;
+
+import android.os.Bundle;
+
+import com.android.car.ui.paintbooth.R;
+import com.android.car.ui.preference.PreferenceFragment;
+
+/**
+ * Fragment to load preferences
+ */
+public class PreferenceDemoFragment extends PreferenceFragment {
+
+    @Override
+    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+        // Load the preferences from an XML resource
+        setPreferencesFromResource(R.xml.preference_samples, rootKey);
+    }
+}
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
new file mode 100644
index 0000000..cb1fdc2
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
@@ -0,0 +1,411 @@
+/*
+ * 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.paintbooth.toolbar;
+
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+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 AppCompatActivity implements InsetsChangedListener {
+
+    private List<MenuItem> mMenuItems = new ArrayList<>();
+    private List<Pair<CharSequence, View.OnClickListener>> mButtons = new ArrayList<>();
+
+    @Override
+    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);
+        toolbar.setLogo(R.drawable.ic_launcher);
+        toolbar.registerOnBackListener(
+                () -> {
+                    if (toolbar.getState() == Toolbar.State.SEARCH
+                            || toolbar.getState() == Toolbar.State.EDIT) {
+                        toolbar.setState(Toolbar.State.SUBPAGE);
+                        return true;
+                    }
+                    return false;
+                });
+
+        mMenuItems.add(MenuItem.builder(this)
+                .setToSearch()
+                .setOnClickListener(i -> toolbar.setState(Toolbar.State.SEARCH))
+                .build());
+
+        toolbar.setMenuItems(mMenuItems);
+
+        mButtons.add(Pair.create("Toggle progress bar", v -> {
+            if (toolbar.getProgressBar().getVisibility() == View.GONE) {
+                toolbar.showProgressBar();
+                Toast.makeText(this, "showing progress bar", Toast.LENGTH_SHORT).show();
+            } else {
+                toolbar.hideProgressBar();
+                Toast.makeText(this, "hiding progress bar", Toast.LENGTH_SHORT).show();
+            }
+        }));
+
+        mButtons.add(Pair.create("Change title", v ->
+                toolbar.setTitle(toolbar.getTitle() + " X")));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_set_xml_resource), v -> {
+            mMenuItems.clear();
+            toolbar.setMenuItems(R.xml.menuitems);
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_icon), v -> {
+            mMenuItems.add(MenuItem.builder(this)
+                    .setToSettings()
+                    .setOnClickListener(i -> Toast.makeText(this, "Clicked",
+                            Toast.LENGTH_SHORT).show())
+                    .build());
+            toolbar.setMenuItems(mMenuItems);
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_untined_icon), v -> {
+            mMenuItems.add(MenuItem.builder(this)
+                    .setIcon(R.drawable.ic_tracklist)
+                    .setTinted(false)
+                    .setOnClickListener(
+                            i -> Toast.makeText(this, "Clicked",
+                                    Toast.LENGTH_SHORT).show())
+                    .build());
+            toolbar.setMenuItems(mMenuItems);
+        }));
+
+        Mutable<Integer> overflowCounter = new Mutable<>(1);
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_overflow), v -> {
+            mMenuItems.add(MenuItem.builder(this)
+                    .setTitle("Foo " + overflowCounter.value)
+                    .setOnClickListener(
+                            i -> Toast.makeText(this, "Clicked",
+                                    Toast.LENGTH_SHORT).show())
+                    .setDisplayBehavior(MenuItem.DisplayBehavior.NEVER)
+                    .build());
+            toolbar.setMenuItems(mMenuItems);
+            overflowCounter.value++;
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_switch), v -> {
+            mMenuItems.add(MenuItem.builder(this)
+                    .setCheckable()
+                    .setOnClickListener(
+                            i ->
+                                    Toast.makeText(this,
+                                            "Checked? " + i.isChecked(),
+                                            Toast.LENGTH_SHORT)
+                                            .show())
+                    .build());
+            toolbar.setMenuItems(mMenuItems);
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_text), v -> {
+            mMenuItems.add(MenuItem.builder(this)
+                    .setTitle("Baz")
+                    .setOnClickListener(
+                            i -> Toast.makeText(this, "Clicked",
+                                    Toast.LENGTH_SHORT).show())
+                    .build());
+            toolbar.setMenuItems(mMenuItems);
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_icon_text), v -> {
+            mMenuItems.add(MenuItem.builder(this)
+                    .setIcon(R.drawable.ic_tracklist)
+                    .setTitle("Bar")
+                    .setShowIconAndTitle(true)
+                    .setOnClickListener(
+                            i -> Toast.makeText(this, "Clicked",
+                                    Toast.LENGTH_SHORT).show())
+                    .build());
+            toolbar.setMenuItems(mMenuItems);
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_untinted_icon_and_text), v -> {
+            mMenuItems.add(MenuItem.builder(this)
+                    .setIcon(R.drawable.ic_tracklist)
+                    .setTitle("Bar")
+                    .setShowIconAndTitle(true)
+                    .setTinted(false)
+                    .setOnClickListener(
+                            i -> Toast.makeText(this, "Clicked",
+                                    Toast.LENGTH_SHORT).show())
+                    .build());
+            toolbar.setMenuItems(mMenuItems);
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_activatable), v -> {
+            mMenuItems.add(MenuItem.builder(this)
+                    .setIcon(R.drawable.ic_tracklist)
+                    .setActivatable()
+                    .setOnClickListener(
+                            i -> Toast.makeText(this, "Clicked",
+                                    Toast.LENGTH_SHORT).show())
+                    .build());
+            toolbar.setMenuItems(mMenuItems);
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_morphing), v -> {
+            mMenuItems.add(MenuItem.builder(this)
+                    .setTitle("Become icon")
+                    .setOnClickListener(i ->
+                            i.setIcon(i.getIcon() == null ? R.drawable.ic_tracklist : 0))
+                    .build());
+            toolbar.setMenuItems(mMenuItems);
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_toggle_visibility),
+                v -> getMenuItem(item -> item.setVisible(!item.isVisible()))));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_toggle_enable),
+                v -> getMenuItem(item -> item.setEnabled(!item.isEnabled()))));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_toggle_perform_click),
+                v -> getMenuItem(MenuItem::performClick)));
+
+        final Drawable altIcon = getDrawable(R.drawable.ic_cut);
+        Map<MenuItem, Drawable> iconBackups = new HashMap<>();
+        mButtons.add(Pair.create(getString(R.string.toolbar_toggle_icon), v ->
+                getMenuItem(item -> {
+                    Drawable currentIcon = item.getIcon();
+                    Drawable newIcon = altIcon;
+                    if (iconBackups.containsKey(item)) {
+                        newIcon = iconBackups.get(item);
+                    }
+                    item.setIcon(newIcon);
+                    iconBackups.put(item, currentIcon);
+                })));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_toggle_show_while_search), v ->
+                toolbar.setShowMenuItemsWhileSearching(
+                        !toolbar.getShowMenuItemsWhileSearching())));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_cycle_nav_button), v -> {
+            Toolbar.NavButtonMode mode = toolbar.getNavButtonMode();
+            if (mode == Toolbar.NavButtonMode.BACK) {
+                toolbar.setNavButtonMode(Toolbar.NavButtonMode.CLOSE);
+            } else if (mode == Toolbar.NavButtonMode.CLOSE) {
+                toolbar.setNavButtonMode(Toolbar.NavButtonMode.DOWN);
+            } else {
+                toolbar.setNavButtonMode(Toolbar.NavButtonMode.BACK);
+            }
+        }));
+
+        Mutable<Boolean> hasLogo = new Mutable<>(true);
+        mButtons.add(Pair.create(getString(R.string.toolbar_toggle_logo), v -> {
+            toolbar.setLogo(hasLogo.value ? 0 : R.drawable.ic_launcher);
+            hasLogo.value = !hasLogo.value;
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_cycle_state), v -> {
+            if (toolbar.getState() == Toolbar.State.SUBPAGE) {
+                toolbar.setState(Toolbar.State.HOME);
+            } else if (toolbar.getState() == Toolbar.State.HOME) {
+                toolbar.setState(Toolbar.State.EDIT);
+            } else {
+                toolbar.setState(Toolbar.State.SUBPAGE);
+            }
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_toggle_search_hint), v -> {
+            if (toolbar.getSearchHint().toString().contentEquals("Foo")) {
+                toolbar.setSearchHint("Bar");
+            } else {
+                toolbar.setSearchHint("Foo");
+            }
+        }));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_toggle_background),
+                v -> toolbar.setBackgroundShown(!toolbar.getBackgroundShown())));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_tab), v -> toolbar.addTab(
+                new TabLayout.Tab(getDrawable(R.drawable.ic_launcher), "Foo"))));
+
+        mButtons.add(Pair.create(getString(R.string.toolbar_add_tab_with_custom_text), v -> {
+            SimpleTextWatcher textWatcher = new SimpleTextWatcher();
+            new AlertDialogBuilder(this)
+                    .setEditBox(null, textWatcher, null)
+                    .setTitle("Enter the text for the title")
+                    .setPositiveButton("Ok", (dialog, which) ->
+                            toolbar.addTab(
+                                    new TabLayout.Tab(
+                                            getDrawable(
+                                                    R.drawable.ic_launcher),
+                                            textWatcher.getText())))
+                    .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) {
+                toolbar.setSearchIcon(0);
+            } else {
+                toolbar.setSearchIcon(R.drawable.ic_launcher);
+            }
+            showingLauncherIcon.value = !showingLauncherIcon.value;
+        }));
+
+        CarUiRecyclerView prv = requireViewById(R.id.list);
+        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();
+    }
+
+    private void getMenuItem(MenuItem.OnClickListener listener) {
+        if (mMenuItems.size() == 1) {
+            listener.onClick(mMenuItems.get(0));
+            return;
+        }
+
+        SimpleTextWatcher textWatcher = new SimpleTextWatcher();
+        new AlertDialogBuilder(this)
+                .setEditBox("", textWatcher, null, InputType.TYPE_CLASS_NUMBER)
+                .setTitle("Enter the index of the MenuItem")
+                .setPositiveButton("Ok", (dialog, which) -> {
+                    try {
+                        MenuItem item = mMenuItems.get(
+                                Integer.parseInt(textWatcher.getText()));
+                        listener.onClick(item);
+                    } catch (NumberFormatException | IndexOutOfBoundsException e) {
+                        Toast.makeText(this, "Invalid index \"" + textWatcher.getText()
+                                        + "\", valid range is 0 to " + (mMenuItems.size() - 1),
+                                Toast.LENGTH_LONG).show();
+                    }
+                }).show();
+    }
+
+    private static class ViewHolder extends RecyclerView.ViewHolder {
+
+        private final Button mButton;
+
+        ViewHolder(View itemView) {
+            super(itemView);
+            mButton = itemView.requireViewById(R.id.button);
+        }
+
+        public void bind(CharSequence title, View.OnClickListener listener) {
+            mButton.setText(title);
+            mButton.setOnClickListener(listener);
+        }
+    }
+
+    private final RecyclerView.Adapter<ViewHolder> mAdapter =
+            new RecyclerView.Adapter<ViewHolder>() {
+                @Override
+                public int getItemCount() {
+                    return mButtons.size();
+                }
+
+                @Override
+                public ViewHolder onCreateViewHolder(ViewGroup parent, int position) {
+                    View item =
+                            LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,
+                                    parent, false);
+                    return new ViewHolder(item);
+                }
+
+                @Override
+                public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+                    Pair<CharSequence, View.OnClickListener> pair = mButtons.get(position);
+                    holder.bind(pair.first, pair.second);
+                }
+            };
+
+    /**
+     * For changing values from lambdas
+     */
+    private static final class Mutable<E> {
+
+        public E value;
+
+        Mutable() {
+            value = null;
+        }
+
+        Mutable(E value) {
+            this.value = value;
+        }
+    }
+
+    /**
+     * Used for getting text from a dialog.
+     */
+    private static final class SimpleTextWatcher implements TextWatcher {
+
+        private String mValue;
+
+        @Override
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        }
+
+        @Override
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+        }
+
+        @Override
+        public void afterTextChanged(Editable s) {
+            mValue = s.toString();
+        }
+
+        public String getText() {
+            return mValue;
+        }
+    }
+}
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
new file mode 100644
index 0000000..ab97ae3
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/widgets/WidgetActivity.java
@@ -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.
+ */
+
+package com.android.car.ui.paintbooth.widgets;
+
+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.
+ */
+public class WidgetActivity extends Activity {
+
+    @Override
+    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/Android.mk b/car-ui-lib/tests/robotests/Android.mk
new file mode 100644
index 0000000..a532bc9
--- /dev/null
+++ b/car-ui-lib/tests/robotests/Android.mk
@@ -0,0 +1,90 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+############################################################
+# CarUi lib just for Robolectric test target.     #
+############################################################
+include $(CLEAR_VARS)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res \
+    $(LOCAL_PATH)/tests/robotests/res \
+
+LOCAL_PACKAGE_NAME := CarUi
+LOCAL_PRIVATE_PLATFORM_APIS := true
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_JAVA_LIBRARIES := android.car
+
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+    car-ui-lib
+
+include $(BUILD_PACKAGE)
+
+################################################
+# Car Ui Robolectric test target. #
+################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := CarUiRoboTests
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_JAVA_RESOURCE_DIRS := config
+
+# Include the testing libraries
+LOCAL_JAVA_LIBRARIES := \
+    android.car \
+    robolectric_android-all-stub \
+    Robolectric_all-target \
+    mockito-robolectric-prebuilt \
+    testng \
+    truth-prebuilt
+
+LOCAL_INSTRUMENTATION_FOR := CarUi
+
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+##################################################################
+# Car Ui runner target to run the previous target. #
+##################################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := RunCarUiRoboTests
+
+LOCAL_JAVA_LIBRARIES := \
+    android.car \
+    CarUiRoboTests \
+    robolectric_android-all-stub \
+    Robolectric_all-target \
+    mockito-robolectric-prebuilt \
+    testng \
+    truth-prebuilt
+
+LOCAL_TEST_PACKAGE := CarUi
+
+LOCAL_ROBOTEST_FILES := $(filter-out %/BaseRobolectricTest.java,\
+    $(call find-files-in-subdirs,$(LOCAL_PATH)/src,*Test.java,.))
+
+LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src
+
+include external/robolectric-shadows/run_robotests.mk
diff --git a/car-media-common/res/anim/media_app_selector_fade_in.xml b/car-ui-lib/tests/robotests/AndroidManifest.xml
similarity index 69%
rename from car-media-common/res/anim/media_app_selector_fade_in.xml
rename to car-ui-lib/tests/robotests/AndroidManifest.xml
index 83e154e..b62579f 100644
--- a/car-media-common/res/anim/media_app_selector_fade_in.xml
+++ b/car-ui-lib/tests/robotests/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-    Copyright 2017 The Android Open Source Project
+    Copyright (C) 2019 Google Inc.
 
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
@@ -14,9 +14,6 @@
     See the License for the specific language governing permissions and
     limitations under the License.
 -->
-<set xmlns:android="http://schemas.android.com/apk/res/android">
-    <alpha
-        android:duration="@android:integer/config_mediumAnimTime"
-        android:fromAlpha="0.2"
-        android:toAlpha="1"/>
-</set>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.car.ui">
+</manifest>
diff --git a/car-ui-lib/tests/robotests/build.gradle b/car-ui-lib/tests/robotests/build.gradle
new file mode 100644
index 0000000..b5876a3
--- /dev/null
+++ b/car-ui-lib/tests/robotests/build.gradle
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        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
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+// Library-level build file
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+
+    defaultConfig {
+        minSdkVersion 28
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    sourceSets {
+        main {
+            manifest.srcFile 'AndroidManifest.xml'
+            res.srcDirs = ['res']
+        }
+
+        test {
+            java.srcDirs = ['src']
+        }
+    }
+
+    testOptions {
+        unitTests {
+            includeAndroidResources = true
+        }
+    }
+}
+
+dependencies {
+    implementation project(':')
+    testImplementation "androidx.test.ext:junit:1.1.1"
+    testImplementation "org.robolectric:robolectric:4.0-alpha-3"
+    testImplementation "org.mockito:mockito-core:2.19.0"
+    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/config/robolectric.properties b/car-ui-lib/tests/robotests/config/robolectric.properties
new file mode 100644
index 0000000..8768f6b
--- /dev/null
+++ b/car-ui-lib/tests/robotests/config/robolectric.properties
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2019 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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=packages/apps/Car/libs/car-ui-lib/tests/robotests/AndroidManifest.xml
+sdk=NEWEST_SDK
diff --git a/car-ui-lib/tests/robotests/res/drawable/test_ic_launcher.png b/car-ui-lib/tests/robotests/res/drawable/test_ic_launcher.png
new file mode 100644
index 0000000..2af53a4
--- /dev/null
+++ b/car-ui-lib/tests/robotests/res/drawable/test_ic_launcher.png
Binary files differ
diff --git a/car-ui-lib/tests/robotests/res/layout/test_custom_view.xml b/car-ui-lib/tests/robotests/res/layout/test_custom_view.xml
new file mode 100644
index 0000000..7a52259
--- /dev/null
+++ b/car-ui-lib/tests/robotests/res/layout/test_custom_view.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/text_box_1"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Text Box 1"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/text_box_2"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+    <TextView
+        android:id="@+id/text_box_2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Text Box 2"
+        app:layout_constraintStart_toEndOf="@id/text_box_1"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/tests/robotests/res/layout/test_grid_car_ui_recycler_view.xml
similarity index 61%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/tests/robotests/res/layout/test_grid_car_ui_recycler_view.xml
index c5d298b..c09d1c7 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/tests/robotests/res/layout/test_grid_car_ui_recycler_view.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright 2019 The Android Open Source Project
+  ~ 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.
@@ -16,10 +16,13 @@
   -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <com.android.car.ui.recyclerview.CarUiRecyclerView
+        android:id="@+id/test_prv"
+        app:layoutStyle="grid"
+        app:numOfColumns="4"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
 </FrameLayout>
\ No newline at end of file
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/tests/robotests/res/layout/test_linear_car_ui_recycler_view.xml
similarity index 68%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/tests/robotests/res/layout/test_linear_car_ui_recycler_view.xml
index c5d298b..e3b8218 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/tests/robotests/res/layout/test_linear_car_ui_recycler_view.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright 2019 The Android Open Source Project
+  ~ 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.
@@ -16,10 +16,10 @@
   -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <com.android.car.ui.recyclerview.CarUiRecyclerView
+        android:id="@+id/test_prv"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
 </FrameLayout>
\ No newline at end of file
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
new file mode 100644
index 0000000..20561f2
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/CarUiRobolectricTestRunner.java
@@ -0,0 +1,137 @@
+/*
+ * 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;
+
+import androidx.annotation.NonNull;
+
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.manifest.AndroidManifest;
+import org.robolectric.res.Fs;
+import org.robolectric.res.ResourcePath;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Custom test runner for CarUi. This is needed because the default behavior for
+ * robolectric is just to grab the resource directory in the target package.
+ * We want to override this to add several spanning different projects.
+ */
+public class CarUiRobolectricTestRunner extends RobolectricTestRunner {
+    private static final Map<String, String> AAR_VERSIONS;
+    private static final String SUPPORT_RESOURCE_PATH_TEMPLATE =
+            "jar:file:%3$s/prebuilts/sdk/current/androidx/m2repository/androidx/"
+                    + "%1$s/%1$s/%2$s/%1$s-%2$s.aar!/res";
+    // contraint-layout aar lives in separate path.
+    // Note its path contains a hyphen.
+    private static final String CONSTRAINT_LAYOUT_RESOURCE_PATH_TEMPLATE =
+            "jar:file:%3$s/prebuilts/sdk/current/extras/constraint-layout-x/"
+                    + "%1$s/%2$s/%1$s-%2$s.aar!/res";
+
+    static {
+        AAR_VERSIONS = new HashMap<>();
+        AAR_VERSIONS.put("appcompat", "1.1.0-alpha06");
+        AAR_VERSIONS.put("constraintlayout", "1.1.2");
+        AAR_VERSIONS.put("preference", "1.1.0-alpha06");
+    }
+
+    public CarUiRobolectricTestRunner(Class<?> testClass) throws InitializationError {
+        super(testClass);
+    }
+
+    private static ResourcePath createResourcePath(@NonNull String filePath) {
+        try {
+            return new ResourcePath(null, Fs.fromURL(new URL(filePath)), null);
+        } catch (MalformedURLException e) {
+            throw new RuntimeException("CarUiRobolectricTestRunner failure", e);
+        }
+    }
+
+    /**
+     * Create the resource path for a support library component's JAR.
+     */
+    private static String createSupportResourcePathFromJar(@NonNull String pathRoot,
+            @NonNull String componentId) {
+        if (!AAR_VERSIONS.containsKey(componentId)) {
+            throw new IllegalArgumentException("Unknown component " + componentId
+                    + ". Update test with appropriate component name and version.");
+        }
+        if (componentId.equals("constraintlayout")) {
+            return String.format(CONSTRAINT_LAYOUT_RESOURCE_PATH_TEMPLATE, componentId,
+                    AAR_VERSIONS.get(componentId), pathRoot);
+        }
+        return String.format(SUPPORT_RESOURCE_PATH_TEMPLATE, componentId,
+                AAR_VERSIONS.get(componentId), pathRoot);
+    }
+
+    /**
+     * We modify the AndroidManifest such that we can add required resources.
+     */
+    @Override
+    protected AndroidManifest getAppManifest(Config config) {
+        try {
+            final URL appRoot;
+            final String rootRelativePath;
+            // Root path is workspace root when run from command line and module root when run from
+            // Android Studio.
+            if (new File(System.getProperty("user.dir")).getName().equals("robotests")) {
+                rootRelativePath = "../../../../../../../.";
+                appRoot = new File("../../.").toURI().toURL();
+            } else {
+                appRoot = new URL("file:packages/apps/Car/libs/car-ui-lib/");
+                rootRelativePath = "./";
+            }
+
+            // Using the manifest file's relative path, we can figure out the application directory.
+            URL manifestPath = new URL(appRoot, "AndroidManifest.xml");
+            URL resDir = new URL(appRoot, "tests/robotests/res");
+            URL assetsDir = new URL(appRoot, config.assetDir());
+
+            // By adding any resources from libraries we need to the AndroidManifest, we can access
+            // them from within the parallel universe's resource loader.
+            return new AndroidManifest(Fs.fromURL(manifestPath), Fs.fromURL(resDir),
+                    Fs.fromURL(assetsDir)) {
+                @Override
+                public List<ResourcePath> getIncludedResourcePaths() {
+                    List<ResourcePath> paths = super.getIncludedResourcePaths();
+                    paths.add(createResourcePath(
+                            String.format("file:%s/packages/apps/Car/libs/car-ui-lib/res",
+                                    rootRelativePath)));
+
+                    // Support library resources. These need to point to the prebuilts of support
+                    // library and not the source.
+                    paths.add(createResourcePath(
+                            createSupportResourcePathFromJar(rootRelativePath, "appcompat")));
+                    paths.add(createResourcePath(createSupportResourcePathFromJar(rootRelativePath,
+                            "constraintlayout")));
+                    paths.add(createResourcePath(
+                            createSupportResourcePathFromJar(rootRelativePath, "preference")));
+
+                    return paths;
+                }
+            };
+        } catch (MalformedURLException e) {
+            throw new RuntimeException("CarUiRobolectricTestRunner failure", e);
+        }
+    }
+}
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/TestConfig.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/TestConfig.java
new file mode 100644
index 0000000..46a9d0c
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/TestConfig.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public class TestConfig {
+    public static final int SDK_VERSION = 28;
+    public static final String MANIFEST_PATH =
+            "packages/apps/Car/car-ui-lib/AndroidManifest.xml";
+}
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
new file mode 100644
index 0000000..f404f6c
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiListItemTest.java
@@ -0,0 +1,299 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.Switch;
+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;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(CarUiRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class CarUiListItemTest {
+
+    private CarUiRecyclerView mListView;
+    private Context mContext;
+
+    @Mock
+    CarUiContentListItem.OnCheckedChangeListener mOnCheckedChangeListener;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = CarUiTestUtil.getMockContext();
+        mListView = new CarUiRecyclerView(mContext);
+    }
+
+    private CarUiListItemAdapter.ListItemViewHolder getListItemViewHolderAtPosition(int position) {
+        return (CarUiListItemAdapter.ListItemViewHolder) mListView.findViewHolderForAdapterPosition(
+                position);
+    }
+
+    private View getListItemTitleAtPosition(int position) {
+        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.title);
+    }
+
+    private View getListItemBodyAtPosition(int position) {
+        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.body);
+    }
+
+    private View getListItemIconContainerAtPosition(int position) {
+        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.icon_container);
+    }
+
+    private View getListItemActionContainerAtPosition(int position) {
+        return getListItemViewHolderAtPosition(position)
+                .itemView.findViewById(R.id.action_container);
+    }
+
+    private Switch getListItemSwitchAtPosition(int position) {
+        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.switch_widget);
+    }
+
+    private CheckBox getListItemCheckBoxAtPosition(int position) {
+        return getListItemViewHolderAtPosition(position)
+                .itemView.findViewById(R.id.checkbox_widget);
+    }
+
+    private View getListItemIconAtPosition(int position) {
+        return getListItemViewHolderAtPosition(position).itemView.findViewById(R.id.icon);
+    }
+
+    private CarUiListItemAdapter.HeaderViewHolder getHeaderViewHolderAtPosition(int position) {
+        return (CarUiListItemAdapter.HeaderViewHolder) mListView.findViewHolderForAdapterPosition(
+                position);
+    }
+
+    private TextView getHeaderViewHolderTitleAtPosition(int position) {
+        return getHeaderViewHolderAtPosition(position).itemView.findViewById(R.id.title);
+    }
+
+    private TextView getHeaderViewHolderBodyAtPosition(int position) {
+        return getHeaderViewHolderAtPosition(position).itemView.findViewById(R.id.body);
+    }
+
+    private void updateRecyclerViewAdapter(CarUiListItemAdapter adapter) {
+        mListView.setAdapter(adapter);
+
+        // Force CarUiRecyclerView and the nested RecyclerView to be laid out.
+        mListView.measure(0, 0);
+        mListView.layout(0, 0, 100, 10000);
+
+        if (mListView != null) {
+            mListView.measure(0, 0);
+            mListView.layout(0, 0, 100, 10000);
+        }
+
+        // Required to init nested RecyclerView
+        mListView.getViewTreeObserver().dispatchOnGlobalLayout();
+    }
+
+    @Test
+    public void testItemVisibility_withTitle() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+        item.setTitle("Test title");
+        items.add(item);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        assertThat(getListItemTitleAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemBodyAtPosition(0).getVisibility()).isNotEqualTo(View.VISIBLE);
+        assertThat(getListItemIconContainerAtPosition(0).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+        assertThat(getListItemActionContainerAtPosition(0).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void testItemVisibility_withTitle_withBody() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+        item.setTitle("Test title");
+        item.setBody("Test body");
+        items.add(item);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        assertThat(getListItemTitleAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemBodyAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemIconContainerAtPosition(0).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+        assertThat(getListItemActionContainerAtPosition(0).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void testItemVisibility_withTitle_withIcon() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.NONE);
+        item.setTitle("Test title");
+        item.setIcon(mContext.getDrawable(R.drawable.car_ui_icon_close));
+        items.add(item);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        assertThat(getListItemTitleAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemBodyAtPosition(0).getVisibility()).isNotEqualTo(View.VISIBLE);
+        assertThat(getListItemIconContainerAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemIconAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemActionContainerAtPosition(0).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void testItemVisibility_withTitle_withCheckbox() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.CHECK_BOX);
+        item.setTitle("Test title");
+        items.add(item);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        assertThat(getListItemTitleAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemBodyAtPosition(0).getVisibility()).isNotEqualTo(View.VISIBLE);
+        assertThat(getListItemIconContainerAtPosition(0).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+        assertThat(getListItemActionContainerAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemSwitchAtPosition(0).getVisibility()).isNotEqualTo(View.VISIBLE);
+        assertThat(getListItemCheckBoxAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemCheckBoxAtPosition(0).isChecked()).isEqualTo(false);
+    }
+
+    @Test
+    public void testItemVisibility_withTitle_withBody_withSwitch() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.SWITCH);
+        item.setTitle("Test title");
+        item.setBody("Body text");
+        items.add(item);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        assertThat(getListItemTitleAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemBodyAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemIconContainerAtPosition(0).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+        assertThat(getListItemActionContainerAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemSwitchAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getListItemSwitchAtPosition(0).isChecked()).isEqualTo(false);
+        assertThat(getListItemCheckBoxAtPosition(0).getVisibility()).isNotEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void testCheckedState_switch() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.SWITCH);
+        item.setTitle("Test title");
+        item.setOnCheckedChangeListener(mOnCheckedChangeListener);
+        item.setChecked(true);
+        items.add(item);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        Switch switchWidget = getListItemSwitchAtPosition(0);
+
+        assertThat(switchWidget.isChecked()).isEqualTo(true);
+        switchWidget.performClick();
+        assertThat(switchWidget.isChecked()).isEqualTo(false);
+        verify(mOnCheckedChangeListener, times(1))
+                .onCheckedChanged(item, false);
+    }
+
+    @Test
+    public void testCheckedState_checkbox() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CarUiContentListItem item = new CarUiContentListItem(CarUiContentListItem.Action.CHECK_BOX);
+        item.setTitle("Test title");
+        item.setOnCheckedChangeListener(mOnCheckedChangeListener);
+        items.add(item);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        CheckBox checkBox = getListItemCheckBoxAtPosition(0);
+
+        assertThat(checkBox.isChecked()).isEqualTo(false);
+        checkBox.performClick();
+        assertThat(checkBox.isChecked()).isEqualTo(true);
+        verify(mOnCheckedChangeListener, times(1))
+                .onCheckedChanged(item, true);
+    }
+
+    @Test
+    public void testHeader_onlyTitle() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CharSequence title = "Test header";
+        CarUiHeaderListItem header = new CarUiHeaderListItem(title);
+        items.add(header);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        assertThat(getHeaderViewHolderTitleAtPosition(0).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(getHeaderViewHolderTitleAtPosition(0).getText()).isEqualTo(title);
+        assertThat(getHeaderViewHolderBodyAtPosition(0).getVisibility()).isNotEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void testHeader_titleAndBody() {
+        List<CarUiListItem> items = new ArrayList<>();
+
+        CharSequence title = "Test header";
+        CharSequence body = "With body text";
+
+        CarUiHeaderListItem header = new CarUiHeaderListItem(title, body);
+        items.add(header);
+
+        updateRecyclerViewAdapter(new CarUiListItemAdapter(items));
+
+        TextView titleView = getHeaderViewHolderTitleAtPosition(0);
+        TextView bodyView = getHeaderViewHolderBodyAtPosition(0);
+
+        assertThat(titleView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(titleView.getText()).isEqualTo(title);
+        assertThat(bodyView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(bodyView.getText()).isEqualTo(body);
+    }
+}
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
new file mode 100644
index 0000000..6c8954c
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewAdapterTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+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;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+@RunWith(CarUiRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class CarUiRecyclerViewAdapterTest {
+
+    private Context mContext;
+    private CarUiRecyclerViewAdapter mCarUiRecyclerViewAdapter;
+
+    @Mock
+    private ViewGroup mParent;
+    @Mock
+    private ViewGroup.LayoutParams mLayoutParams;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = CarUiTestUtil.getMockContext();
+        mCarUiRecyclerViewAdapter = new CarUiRecyclerViewAdapter();
+    }
+
+    @Test
+    public void getItemCount_shouldAlwaysBeOne() {
+        assertThat(mCarUiRecyclerViewAdapter.getItemCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void onCreateViewHolder_frameLayoutNotNull() {
+
+        when(mParent.getContext()).thenReturn(mContext);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        CarUiRecyclerViewAdapter.NestedRowViewHolder nestedRowViewHolder =
+                mCarUiRecyclerViewAdapter.onCreateViewHolder(mParent, 0);
+
+        assertThat(nestedRowViewHolder.frameLayout).isNotNull();
+    }
+}
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
new file mode 100644
index 0000000..f4d98f7
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiRecyclerViewTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+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.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;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = RuntimeEnvironment.application;
+    }
+
+    @Test
+    public void onHeightChanged_shouldAddTheValueToInitialTopValue() {
+        mView = LayoutInflater.from(mContext)
+                .inflate(R.layout.test_linear_car_ui_recycler_view, null);
+
+        mCarUiRecyclerView = mView.findViewById(R.id.test_prv);
+
+        assertThat(mCarUiRecyclerView.getPaddingBottom()).isEqualTo(0);
+        assertThat(mCarUiRecyclerView.getPaddingTop()).isEqualTo(0);
+        assertThat(mCarUiRecyclerView.getPaddingStart()).isEqualTo(0);
+        assertThat(mCarUiRecyclerView.getPaddingEnd()).isEqualTo(0);
+
+        mCarUiRecyclerView.onHeightChanged(10);
+
+        assertThat(mCarUiRecyclerView.getPaddingTop()).isEqualTo(10);
+        assertThat(mCarUiRecyclerView.getPaddingBottom()).isEqualTo(0);
+        assertThat(mCarUiRecyclerView.getPaddingStart()).isEqualTo(0);
+        assertThat(mCarUiRecyclerView.getPaddingEnd()).isEqualTo(0);
+    }
+
+    @Test
+    public void setAdapter_shouldInitializeLinearLayoutManager() {
+        mView = LayoutInflater.from(mContext)
+                .inflate(R.layout.test_linear_car_ui_recycler_view, null);
+
+        mCarUiRecyclerView = mView.findViewById(R.id.test_prv);
+
+        assertThat(mCarUiRecyclerView.getEffectiveLayoutManager()).isInstanceOf(
+                LinearLayoutManager.class);
+    }
+
+    @Test
+    public void setAdapter_shouldInitializeGridLayoutManager() {
+        mView = LayoutInflater.from(mContext)
+                .inflate(R.layout.test_grid_car_ui_recycler_view, null);
+
+        mCarUiRecyclerView = mView.findViewById(R.id.test_prv);
+
+        assertThat(mCarUiRecyclerView.getEffectiveLayoutManager()).isInstanceOf(
+                GridLayoutManager.class);
+    }
+
+    @Test
+    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).isNotNull();
+    }
+
+    @Test
+    public void init_shouldHaveGridLayout() {
+        mCarUiRecyclerView = new CarUiRecyclerView(mContext,
+                Robolectric.buildAttributeSet().addAttribute(R.attr.layoutStyle, "grid").build());
+        assertThat(mCarUiRecyclerView.getEffectiveLayoutManager()).isInstanceOf(
+                GridLayoutManager.class);
+    }
+}
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
new file mode 100644
index 0000000..052304c
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSmoothScrollerTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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 static androidx.recyclerview.widget.LinearSmoothScroller.SNAP_TO_START;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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;
+    private CarUiSmoothScroller mCarUiSmoothScroller;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mCarUiSmoothScroller = new CarUiSmoothScroller(mContext);
+    }
+
+    @Test
+    public void calculateTimeForScrolling_shouldInitializeAllValues() {
+        assertThat(mCarUiSmoothScroller.mMillisecondsPerInch).isNotEqualTo(0);
+        assertThat(mCarUiSmoothScroller.mDecelerationTimeDivisor).isNotEqualTo(0);
+        assertThat(mCarUiSmoothScroller.mMillisecondsPerPixel).isNotEqualTo(0);
+        assertThat(mCarUiSmoothScroller.mInterpolator).isNotNull();
+        assertThat(mCarUiSmoothScroller.mDensityDpi).isNotEqualTo(0);
+    }
+
+    @Test
+    public void getVerticalSnapPreference_shouldReturnSnapToStart() {
+        assertThat(mCarUiSmoothScroller.getVerticalSnapPreference()).isEqualTo(SNAP_TO_START);
+    }
+
+    @Test
+    public void calculateTimeForScrolling_shouldReturnMultiplierOfMillisecondsPerPixel() {
+        assertThat(mCarUiSmoothScroller.calculateTimeForScrolling(20)).isEqualTo(
+                (int) Math.ceil(Math.abs(20) * mCarUiSmoothScroller.mMillisecondsPerPixel));
+    }
+
+    @Test
+    public void calculateTimeForDeceleration_shouldReturnNotBeZero() {
+        assertThat(mCarUiSmoothScroller.calculateTimeForDeceleration(20)).isNotEqualTo(0);
+    }
+}
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
new file mode 100644
index 0000000..b846103
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/CarUiSnapHelperTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+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;
+
+@RunWith(RobolectricTestRunner.class)
+public class CarUiSnapHelperTest {
+
+    private Context mContext;
+    private CarUiSnapHelper mCarUiSnapHelper;
+
+    @Mock
+    private RecyclerView mRecyclerView;
+    @Mock
+    private LinearLayoutManager mLayoutManager;
+    @Mock
+    private RecyclerView.Adapter mAdapter;
+    @Mock
+    private View mChild;
+    @Mock
+    private RecyclerView.LayoutParams mLayoutParams;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+
+        mCarUiSnapHelper = new CarUiSnapHelper(mContext);
+
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        mCarUiSnapHelper.attachToRecyclerView(mRecyclerView);
+    }
+
+    @Test
+    public void calculateDistanceToFinalSnap_shouldReturnTopMarginDifference() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+        when(mLayoutManager.getItemCount()).thenReturn(1);
+        when(mLayoutManager.canScrollVertically()).thenReturn(true);
+        when(mLayoutManager.getChildCount()).thenReturn(1);
+        // some delta
+        when(mLayoutManager.getDecoratedTop(any())).thenReturn(10);
+        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
+
+        int[] distance = mCarUiSnapHelper.calculateDistanceToFinalSnap(mLayoutManager, mChild);
+
+        assertThat(distance[1]).isEqualTo(10);
+    }
+
+    @Test
+    public void calculateScrollDistance_shouldScrollHeightOfView() {
+        when(mRecyclerView.getLayoutManager()).thenReturn(mLayoutManager);
+        when(mLayoutManager.getItemCount()).thenReturn(1);
+        when(mLayoutManager.canScrollVertically()).thenReturn(true);
+        when(mLayoutManager.getChildCount()).thenReturn(1);
+        // some delta
+        when(mLayoutManager.getDecoratedTop(any())).thenReturn(10);
+        when(mChild.getLayoutParams()).thenReturn(mLayoutParams);
+        when(mLayoutManager.getChildAt(0)).thenReturn(mChild);
+        when(mLayoutManager.getHeight()).thenReturn(-50);
+
+        int[] distance = mCarUiSnapHelper.calculateScrollDistance(0, 10);
+
+        assertThat(distance[1]).isEqualTo(50);
+    }
+}
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
new file mode 100644
index 0000000..612f463
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/recyclerview/DefaultScrollBarTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+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;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+@RunWith(CarUiRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class DefaultScrollBarTest {
+
+    private Context mContext;
+    private ScrollBar mScrollBar;
+
+    @Mock
+    private RecyclerView mRecyclerView;
+    @Mock
+    private FrameLayout mParent;
+    @Mock
+    private FrameLayout.LayoutParams mLayoutParams;
+    @Mock
+    private RecyclerView.RecycledViewPool mRecycledViewPool;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = CarUiTestUtil.getMockContext();
+        mScrollBar = new DefaultScrollBar();
+    }
+
+    @Test
+    public void initialize_shouldInitializeScrollListener() {
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        when(mRecyclerView.getParent()).thenReturn(mParent);
+        when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        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)
+        verify(mRecyclerView, times(2)).addOnScrollListener(
+                any(RecyclerView.OnScrollListener.class));
+    }
+
+    @Test
+    public void initialize_shouldSetMaxRecyclerViews() {
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        when(mRecyclerView.getParent()).thenReturn(mParent);
+        when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        View scrollView = LayoutInflater.from(mContext).inflate(
+                R.layout.car_ui_recyclerview_scrollbar, null);
+        mScrollBar.initialize(mRecyclerView, scrollView);
+
+        verify(mRecycledViewPool).setMaxRecycledViews(0, 12);
+    }
+
+    @Test
+    public void initialize_shouldNotHaveFlingListener() {
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        when(mRecyclerView.getParent()).thenReturn(mParent);
+        when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        View scrollView = LayoutInflater.from(mContext).inflate(
+                R.layout.car_ui_recyclerview_scrollbar, null);
+        mScrollBar.initialize(mRecyclerView, scrollView);
+
+        verify(mRecyclerView).setOnFlingListener(null);
+    }
+
+    @Test
+    public void setPadding_shouldSetStartAndEndPadding() {
+        when(mRecyclerView.getContext()).thenReturn(mContext);
+        when(mRecyclerView.getParent()).thenReturn(mParent);
+        when(mRecyclerView.getRecycledViewPool()).thenReturn(mRecycledViewPool);
+        when(mParent.generateLayoutParams(any())).thenReturn(mLayoutParams);
+
+        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;
+
+        assertThat(defaultScrollBar.mPaddingStart).isEqualTo(10);
+        assertThat(defaultScrollBar.mPaddingEnd).isEqualTo(20);
+    }
+
+    @Test
+    public void setPadding_shouldThrowErrorWithoutInitialization() {
+        assertThrows(NullPointerException.class, () -> mScrollBar.setPadding(10, 20));
+    }
+
+    @Test
+    public void requestLayout_shouldThrowErrorWithoutInitialization() {
+        assertThrows(NullPointerException.class, () -> mScrollBar.requestLayout());
+    }
+}
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/ToolbarTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java
new file mode 100644
index 0000000..78aab7d
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/toolbar/ToolbarTest.java
@@ -0,0 +1,467 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.View;
+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.annotation.Config;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(CarUiRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+        shadows = {ExtendedShadowTypeface.class, ShadowAsyncLayoutInflater.class})
+public class ToolbarTest {
+    private Context mContext;
+    private Resources mResources;
+    private Toolbar mToolbar;
+
+    @Before
+    public void setUp() {
+        mContext = CarUiTestUtil.getMockContext();
+        mResources = mContext.getResources();
+        mToolbar = new Toolbar(mContext);
+    }
+
+    @Test
+    public void getters_nochanges_shouldReturnDefaults() {
+        assertThat(mToolbar.getBackgroundShown()).isEqualTo(true);
+        assertThat(mToolbar.getShowMenuItemsWhileSearching()).isEqualTo(false);
+        assertThat(mToolbar.getState()).isEquivalentAccordingToCompareTo(Toolbar.State.HOME);
+        assertThat(mToolbar.getNavButtonMode()).isEquivalentAccordingToCompareTo(
+                Toolbar.NavButtonMode.BACK);
+    }
+
+    @Test
+    public void setState_subpage_shouldCauseGetStateToReturnSubpage() {
+        mToolbar.setState(Toolbar.State.SUBPAGE);
+
+        assertThat(mToolbar.getState()).isEquivalentAccordingToCompareTo(Toolbar.State.SUBPAGE);
+    }
+
+    @Test
+    public void setters_and_getters_test() {
+        mToolbar.setTitle("Foo");
+        mToolbar.setSearchHint("Foo2");
+        mToolbar.setBackgroundShown(false);
+        mToolbar.setShowMenuItemsWhileSearching(true);
+        mToolbar.setState(Toolbar.State.SUBPAGE);
+        mToolbar.setNavButtonMode(Toolbar.NavButtonMode.CLOSE);
+
+        assertThat(mToolbar.getTitle().toString()).isEqualTo("Foo");
+        assertThat(mToolbar.getSearchHint().toString()).isEqualTo("Foo2");
+        assertThat(mToolbar.getBackgroundShown()).isEqualTo(false);
+        assertThat(mToolbar.getShowMenuItemsWhileSearching()).isEqualTo(true);
+        assertThat(mToolbar.getState()).isEquivalentAccordingToCompareTo(Toolbar.State.SUBPAGE);
+        assertThat(mToolbar.getNavButtonMode()).isEquivalentAccordingToCompareTo(
+                Toolbar.NavButtonMode.CLOSE);
+    }
+
+    @Test
+    public void showLogo_whenSet_andStateIsHome() {
+        mToolbar.setState(Toolbar.State.HOME);
+        mToolbar.setLogo(R.drawable.test_ic_launcher);
+
+        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() {
+        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(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 showTitleLogo_whenSet_andStateIsNotHome() {
+        mToolbar.setState(Toolbar.State.SUBPAGE);
+        mToolbar.setLogo(R.drawable.test_ic_launcher);
+
+        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
+    public void hideLogo_andTitleLogo_whenNotSet_andStateIsHome() {
+        mToolbar.setState(Toolbar.State.HOME);
+        mToolbar.setLogo(0);
+
+        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).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_title_logo_container).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void registerOnBackListener_whenBackIsPressed_shouldCallListener() {
+        mToolbar.setState(Toolbar.State.SUBPAGE);
+        Mutable<Integer> timesBackPressed = new Mutable<>(0);
+        Toolbar.OnBackListener listener = () -> {
+            timesBackPressed.value++;
+            return false;
+        };
+
+        mToolbar.registerOnBackListener(listener);
+        pressBack();
+
+        assertThat(timesBackPressed.value).isEqualTo(1);
+    }
+
+    @Test
+    public void testState_twoRow_withTitle_withTabs() {
+        when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(true);
+
+        Toolbar toolbar = new Toolbar(mContext);
+        assertThat(toolbar.isTabsInSecondRow()).isTrue();
+
+        // Set title and tabs for toolbar.
+        toolbar.setTitle("Test title");
+        toolbar.addTab(new TabLayout.Tab(mContext.getDrawable(R.drawable.test_ic_launcher), "Foo"));
+        toolbar.addTab(new TabLayout.Tab(mContext.getDrawable(R.drawable.test_ic_launcher), "Foo"));
+
+        // Toolbar should display two rows, showing both title and tabs.
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_tabs).getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_title).getVisibility()).isEqualTo(
+                View.VISIBLE);
+    }
+
+    @Test
+    public void testState_twoRow_withTitle() {
+        when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(true);
+
+        Toolbar toolbar = new Toolbar(mContext);
+        assertThat(toolbar.isTabsInSecondRow()).isTrue();
+
+        toolbar.setTitle("Test title");
+
+        // Toolbar should display two rows, but no tabs are set so they should not be visible.
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_title).getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_tabs).getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+    }
+
+    @Test
+    public void testState_twoRow_withTabs() {
+        when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(true);
+
+        Toolbar toolbar = new Toolbar(mContext);
+        assertThat(toolbar.isTabsInSecondRow()).isTrue();
+        toolbar.addTab(new TabLayout.Tab(mContext.getDrawable(R.drawable.test_ic_launcher), "Foo"));
+        toolbar.addTab(new TabLayout.Tab(mContext.getDrawable(R.drawable.test_ic_launcher), "Foo"));
+
+        // Toolbar should display two rows with an empty title and tabs.
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_tabs).getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_title).getVisibility()).isEqualTo(
+                View.VISIBLE);
+    }
+
+    @Test
+    public void testState_oneRow_withTitle_withTabs() {
+        when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(false);
+
+        Toolbar toolbar = new Toolbar(mContext);
+        assertThat(toolbar.isTabsInSecondRow()).isFalse();
+
+        // Set title and tabs for toolbar.
+        toolbar.setTitle("Test title");
+        toolbar.addTab(new TabLayout.Tab(mContext.getDrawable(R.drawable.test_ic_launcher), "Foo"));
+        toolbar.addTab(new TabLayout.Tab(mContext.getDrawable(R.drawable.test_ic_launcher), "Foo"));
+
+        // With only one row available, toolbar will only show tabs and not the title.
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_tabs).getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_title).getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+    }
+
+    @Test
+    public void testState_oneRow_withTitle() {
+        when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(false);
+
+        Toolbar toolbar = new Toolbar(mContext);
+        assertThat(toolbar.isTabsInSecondRow()).isFalse();
+
+        toolbar.setTitle("Test title");
+
+        // Toolbar should display one row with the title and no tabs.
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_tabs).getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_title).getVisibility()).isEqualTo(
+                View.VISIBLE);
+    }
+
+    @Test
+    public void testState_oneRow_withTabs() {
+        when(mResources.getBoolean(R.bool.car_ui_toolbar_tabs_on_second_row)).thenReturn(false);
+
+
+        Toolbar toolbar = new Toolbar(mContext);
+        assertThat(toolbar.isTabsInSecondRow()).isFalse();
+
+        toolbar.addTab(new TabLayout.Tab(mContext.getDrawable(R.drawable.test_ic_launcher), "Foo"));
+        toolbar.addTab(new TabLayout.Tab(mContext.getDrawable(R.drawable.test_ic_launcher), "Foo"));
+
+        // Toolbar should display one row with only tabs.
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_tabs).getVisibility()).isEqualTo(
+                View.VISIBLE);
+        assertThat(toolbar.findViewById(R.id.car_ui_toolbar_title).getVisibility()).isNotEqualTo(
+                View.VISIBLE);
+    }
+
+    @Test
+    public void registerOnBackListener_whenListenerRegisteredTwice_shouldntCallListenerTwice() {
+        mToolbar.setState(Toolbar.State.SUBPAGE);
+        Mutable<Integer> timesBackPressed = new Mutable<>(0);
+        Toolbar.OnBackListener listener = () -> {
+            timesBackPressed.value++;
+            return false;
+        };
+
+        // Registering a second time shouldn't do anything
+        mToolbar.registerOnBackListener(listener);
+        mToolbar.registerOnBackListener(listener);
+        pressBack();
+
+        assertThat(timesBackPressed.value).isEqualTo(1);
+    }
+
+    @Test
+    public void unregisterOnBackListener_previouslyRegisteredListener_shouldUnregister() {
+        mToolbar.setState(Toolbar.State.SUBPAGE);
+        Mutable<Integer> timesBackPressed = new Mutable<>(0);
+        Toolbar.OnBackListener listener = () -> {
+            timesBackPressed.value++;
+            return false;
+        };
+
+        mToolbar.registerOnBackListener(listener);
+        mToolbar.unregisterOnBackListener(listener);
+        pressBack();
+
+        assertThat(timesBackPressed.value).isEqualTo(0);
+    }
+
+    @Test
+    public void menuItems_builder_id() {
+        MenuItem item = MenuItem.builder(mContext)
+                .setId(5)
+                .build();
+
+        assertThat(item.getId()).isEqualTo(5);
+    }
+
+    @Test
+    public void menuItems_setId_shouldWork() {
+        MenuItem item = MenuItem.builder(mContext).build();
+
+        assertThat(item.getId()).isEqualTo(View.NO_ID);
+
+        item.setId(7);
+
+        assertThat(item.getId()).isEqualTo(7);
+    }
+
+    @Test
+    public void menuItems_whenClicked_shouldCallListener() {
+        assertThat(getMenuItemCount()).isEqualTo(0);
+
+        Mutable<Boolean> button1Clicked = new Mutable<>(false);
+        Mutable<Boolean> button2Clicked = new Mutable<>(false);
+        mToolbar.setMenuItems(Arrays.asList(
+                createMenuItem(i -> button1Clicked.value = true),
+                createMenuItem(i -> button2Clicked.value = true)));
+
+        assertThat(getMenuItemCount()).isEqualTo(2);
+
+        getMenuItemView(0).performClick();
+
+        assertThat(button1Clicked.value).isTrue();
+
+        getMenuItemView(1).performClick();
+
+        assertThat(button2Clicked.value).isTrue();
+    }
+
+    @Test
+    public void menuItems_null_shouldRemoveExistingMenuItems() {
+        mToolbar.setMenuItems(Arrays.asList(
+                createMenuItem(i -> {
+                }),
+                createMenuItem(i -> {
+                })));
+
+        assertThat(getMenuItemCount()).isEqualTo(2);
+
+        mToolbar.setMenuItems(null);
+
+        assertThat(getMenuItemCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void menuItems_setVisibility_shouldDefaultToShown() {
+        MenuItem item = createMenuItem(i -> {
+        });
+        mToolbar.setMenuItems(Collections.singletonList(item));
+
+        assertThat(getMenuItemView(0).getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void menuItems_setVisibility_shouldHide() {
+        MenuItem item = createMenuItem(i -> {
+        });
+        mToolbar.setMenuItems(Collections.singletonList(item));
+
+        item.setVisible(false);
+        assertThat(getMenuItemView(0).getVisibility()).isNotEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void menuItems_setVisibility_shouldReshowAfterHiding() {
+        MenuItem item = createMenuItem(i -> {
+        });
+        mToolbar.setMenuItems(Collections.singletonList(item));
+
+        item.setVisible(false);
+        item.setVisible(true);
+        assertThat(getMenuItemView(0).getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void menuItems_equalItems_shouldntRecreateViews() {
+        List<MenuItem> menuItems = Arrays.asList(
+                createMenuItem(i -> {
+                }),
+                createMenuItem(i -> {
+                }));
+        mToolbar.setMenuItems(menuItems);
+
+        assertThat(getMenuItemCount()).isEqualTo(2);
+
+        View firstMenuItemView = getMenuItemView(0);
+
+        mToolbar.setMenuItems(menuItems);
+
+        assertThat(firstMenuItemView).isSameAs(getMenuItemView(0));
+    }
+
+    @Test
+    public void menuItems_searchScreen_shouldHideMenuItems() {
+        mToolbar.setMenuItems(Arrays.asList(
+                MenuItem.builder(mContext).setToSearch().build(),
+                createMenuItem(i -> {
+                })));
+
+        mToolbar.setShowMenuItemsWhileSearching(false);
+        mToolbar.setState(Toolbar.State.SEARCH);
+
+        assertThat(mToolbar.findViewById(R.id.car_ui_toolbar_menu_items_container).getVisibility())
+                .isNotEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void menuItems_showMenuItemsWhileSearching() {
+        mToolbar.setMenuItems(Arrays.asList(
+                MenuItem.builder(mContext).setToSearch().build(),
+                createMenuItem(i -> {
+                })));
+
+        mToolbar.setShowMenuItemsWhileSearching(true);
+        mToolbar.setState(Toolbar.State.SEARCH);
+
+        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) {
+        return MenuItem.builder(mContext)
+                .setTitle("Button!")
+                .setOnClickListener(listener)
+                .build();
+    }
+
+    private int getMenuItemCount() {
+        return mToolbar.getMenuItems().size();
+    }
+
+    private View getMenuItemView(int index) {
+        return ((ViewGroup) mToolbar
+                .findViewById(R.id.car_ui_toolbar_menu_items_container))
+                .getChildAt(index);
+    }
+
+    private void pressBack() {
+        mToolbar.findViewById(R.id.car_ui_toolbar_nav_icon_container).performClick();
+    }
+
+    private static class Mutable<T> {
+        public T value;
+
+        Mutable(T value) {
+            this.value = value;
+        }
+    }
+
+}
diff --git a/car-ui-lib/tests/robotests/src/com/android/car/ui/utils/CarUxRestrictionsUtilTest.java b/car-ui-lib/tests/robotests/src/com/android/car/ui/utils/CarUxRestrictionsUtilTest.java
new file mode 100644
index 0000000..fe6f80c
--- /dev/null
+++ b/car-ui-lib/tests/robotests/src/com/android/car/ui/utils/CarUxRestrictionsUtilTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.utils;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.car.drivingstate.CarUxRestrictions;
+
+import com.android.car.ui.TestConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class CarUxRestrictionsUtilTest {
+    private int[] mRestrictionsArray;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mRestrictionsArray = new int[]{
+                CarUxRestrictions.UX_RESTRICTIONS_NO_DIALPAD,
+                CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD,
+                CarUxRestrictions.UX_RESTRICTIONS_NO_DIALPAD
+                        | CarUxRestrictions.UX_RESTRICTIONS_NO_KEYBOARD,
+                CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED
+        };
+    }
+
+    @Test
+    public void testNullActiveRestriction() {
+        CarUxRestrictions activeRestrictions = null;
+        boolean[] expectedResults = {true, true, true, true};
+        for (int i = 0; i < mRestrictionsArray.length; i++) {
+            boolean actualResult = CarUxRestrictionsUtil.isRestricted(mRestrictionsArray[i],
+                    activeRestrictions);
+            assertThat(actualResult == expectedResults[i]).isTrue();
+        }
+    }
+
+    @Test
+    public void testOneActiveRestriction() {
+        CarUxRestrictions activeRestrictions = new CarUxRestrictions.Builder(/* reqOpt= */true,
+                CarUxRestrictions.UX_RESTRICTIONS_NO_DIALPAD, /* timestamp= */0).build();
+        boolean[] expectedResults = {true, false, true, true};
+        for (int i = 0; i < mRestrictionsArray.length; i++) {
+            boolean actualResult = CarUxRestrictionsUtil.isRestricted(mRestrictionsArray[i],
+                    activeRestrictions);
+            assertThat(actualResult == expectedResults[i]).isTrue();
+        }
+    }
+
+    @Test
+    public void testMultipleActiveRestrictions() {
+        CarUxRestrictions activeRestrictions = new CarUxRestrictions.Builder(/* reqOpt= */true,
+                CarUxRestrictions.UX_RESTRICTIONS_NO_DIALPAD
+                        | CarUxRestrictions.UX_RESTRICTIONS_NO_TEXT_MESSAGE, /* timestamp= */
+                0).build();
+        boolean[] expectedResults = {true, false, true, true};
+        for (int i = 0; i < mRestrictionsArray.length; i++) {
+            boolean actualResult = CarUxRestrictionsUtil.isRestricted(mRestrictionsArray[i],
+                    activeRestrictions);
+            assertThat(actualResult == expectedResults[i]).isTrue();
+        }
+    }
+}
diff --git a/car-ui-lib/tests/rro-base/Android.mk b/car-ui-lib/tests/rro-base/Android.mk
new file mode 100644
index 0000000..2d6730c
--- /dev/null
+++ b/car-ui-lib/tests/rro-base/Android.mk
@@ -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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+CAR_UI_RRO_SET_NAME := base
+CAR_UI_RESOURCE_DIR := $(LOCAL_PATH)/res
+CAR_UI_RRO_TARGETS := \
+  com.android.car.ui.paintbooth \
+  com.android.car.media \
+  com.android.car.dialer
+
+include $(CAR_UI_GENERATE_RRO_SET)
diff --git a/car-ui-lib/tests/rro-base/AndroidManifest.xml b/car-ui-lib/tests/rro-base/AndroidManifest.xml
new file mode 100644
index 0000000..b6b6ffb
--- /dev/null
+++ b/car-ui-lib/tests/rro-base/AndroidManifest.xml
@@ -0,0 +1,56 @@
+<?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.
+-->
+
+<!--
+    Example to use Runtime Resource Overlay(RRO).
+
+    Step 1: Create a new project with AndroidManifest.xml file as shown below.
+        "{{TARGET_PACKAGE_NAME}}" will point to the package which will be overridden by new values.
+        "{{RRO_PACKAGE_NAME}}" will be the current package name of this apk.
+    Step 2: Create new values in the current package that will override the values in the target
+        apk. Path and resource name should be same as the target apk in order to override.
+        Look at car-ui-lib/res to see a list of resources available for customization.
+    Step 3: Update Android.mk variables as needed (see details at generate-rros.mk):
+        CAR_UI_RRO_SET_NAME: general name of this overlay, e.g: base.
+        CAR_UI_RESOURCE_DIR: location of the resources folder, e.g.: $(LOCAL_PATH)/res
+        CAR_UI_RRO_TARGETS: list of package names to overlay
+    Step 4: Build and generate the apk package for this project. Resulting RROs will be located at
+        $OUT/vendor/overlay. A full flashing of a device will install all of them, but they can be
+        installed individually (see below).
+    Step 5: Push the package to "/vendor/overlay/" and reboot. Follow the commands below.
+        # adb root;
+        # adb remount;
+        # adb push <path-to-apk> /vendor/overlay/;
+        Alternatively, to side-load a change, just install the APK as normal (note: the apk
+        will end up at /data/app instead of /vendor/overlay).
+        # adb install -r <path-to-apk>
+    Step 6: Apply by overlay command
+        # "adb shell cmd overlay list;" Output of the same will be as shown below.
+           com.android.car.ui.paintbooth
+           [ ] com.android.car.ui.paintbooth.base.rro
+        # adb shell cmd overlay enable (double-hyphen)user 0 com.android.car.ui.paintbooth.rro
+           to enable the RRO
+        # adb shell cmd overlay disable (double-hyphen)user 0 com.android.car.ui.paintbooth.rro
+           to disable RRO
+        (Be careful to use the right user id, depending on the targeted app)
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="{{RRO_PACKAGE_NAME}}">
+    <application android:hasCode="false"/>
+    <overlay android:priority="10"
+             android:targetPackage="{{TARGET_PACKAGE_NAME}}"/>
+</manifest>
diff --git a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml b/car-ui-lib/tests/rro-base/res/values/colors.xml
similarity index 65%
copy from car-media-common/res/layout/full_play_pause_stop_button_layout.xml
copy to car-ui-lib/tests/rro-base/res/values/colors.xml
index c5d298b..3341ad8 100644
--- a/car-media-common/res/layout/full_play_pause_stop_button_layout.xml
+++ b/car-ui-lib/tests/rro-base/res/values/colors.xml
@@ -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:id="@+id/play_pause_container"
-    android:clipChildren="false"
-    android:focusable="false"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content">
-    <include layout="@layout/play_pause_stop_button_layout"/>
-</FrameLayout>
\ No newline at end of file
+<resources>
+  <color name="dialog_activity_background_color">#ff000f</color>
+</resources>
diff --git a/car-ui-lib/tests/rro-base/res/values/styles.xml b/car-ui-lib/tests/rro-base/res/values/styles.xml
new file mode 100644
index 0000000..b5d20db
--- /dev/null
+++ b/car-ui-lib/tests/rro-base/res/values/styles.xml
@@ -0,0 +1,22 @@
+<?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">
+    <style name="TextAppearance.CarUi.Widget.Toolbar.Title" parent="android:TextAppearance.DeviceDefault">
+        <item name="android:singleLine">true</item>
+        <item name="android:textSize">40sp</item>
+        <item name="android:textColor">#FF00F0</item>
+    </style>
+</resources>
diff --git a/car-ui-lib/tests/tools/quick_rro.py b/car-ui-lib/tests/tools/quick_rro.py
new file mode 100755
index 0000000..3d7d9ea
--- /dev/null
+++ b/car-ui-lib/tests/tools/quick_rro.py
@@ -0,0 +1,307 @@
+#!/usr/bin/env python
+#
+# 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.
+
+from argparse import ArgumentParser as AP, RawDescriptionHelpFormatter
+import os
+import sys
+import re
+import subprocess
+import time
+from hashlib import sha1
+
+def hex_to_letters(hex):
+    """Converts numbers in a hex string to letters.
+
+    Example: 0beec7b5 -> aBEEChBf"""
+    hex = hex.upper()
+    chars = []
+    for char in hex:
+        if ord('0') <= ord(char) <= ord('9'):
+            # Convert 0-9 to a-j
+            chars.append(chr(ord(char) - ord('0') + ord('a')))
+        else:
+            chars.append(char)
+    return ''.join(chars)
+
+def get_package_name(args):
+    """Generates a package name for the quickrro.
+
+    The name is quickrro.<hash>. The hash is based on
+    all of the inputs to the RRO. (package to overlay and resources)
+    The hash will be entirely lowercase/uppercase letters, since
+    android package names can't have numbers."""
+    hash = None
+    if args.resources is not None:
+        args.resources.sort()
+        hash = sha1(''.join(args.resources) + args.package)
+    else:
+        hash = sha1(args.package)
+        for root, dirs, files in os.walk(args.dir):
+            for file in files:
+                path = os.path.join(root, file)
+                hash.update(path)
+                with open(path, 'rb') as f:
+                    while True:
+                        buf = f.read(4096)
+                        if not buf:
+                            break
+                        hash.update(buf)
+
+    result = 'quickrro.' + hex_to_letters(hash.hexdigest())
+    return result
+
+def run_command(command_args):
+    """Returns the stdout of a command, and throws an exception if the command fails"""
+    result = subprocess.Popen(command_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    stdout, stderr = result.communicate()
+
+    stdout = str(stdout)
+    stderr = str(stderr)
+
+    if result.returncode != 0:
+        err = 'command failed: ' + ' '.join(command_args)
+        if len(stdout) > 0:
+            err += '\n' + stdout.strip()
+        if len(stderr) > 0:
+            err += '\n' + stderr.strip()
+        raise Exception(err)
+
+    return stdout
+
+def get_android_dir_priority(dir):
+    """Given the name of a directory under ~/Android/Sdk/platforms, returns an integer priority.
+
+    The directory with the highest priority will be used. Currently android-stable is higest,
+    and then after that the api level is the priority. eg android-28 has priority 28."""
+    if len(dir) == 0:
+        return -1
+    if 'stable' in dir:
+        return 999
+
+    try:
+        return int(dir.split('-')[1])
+    except Exception:
+        pass
+
+    return 0
+
+def find_android_jar(path=None):
+    """Returns the path to framework-res.apk or android.jar, throwing an Exception when not found.
+
+    First looks in the given path. Then looks in $OUT/system/framework/framework-res.apk.
+    Finally, looks in ~/Android/Sdk/platforms."""
+    if path is not None:
+        if os.path.isfile(path):
+            return path
+        else:
+            raise Exception('Invalid path: ' + path)
+
+    framework_res_path = os.path.join(os.environ['OUT'], 'system/framework/framework-res.apk')
+    if os.path.isfile(framework_res_path):
+        return framework_res_path
+
+    sdk_dir = os.path.expanduser('~/Android/Sdk/platforms')
+    best_dir = ''
+    for dir in os.listdir(sdk_dir):
+        if os.path.isdir(os.path.join(sdk_dir, dir)):
+            if get_android_dir_priority(dir) > get_android_dir_priority(best_dir):
+                best_dir = dir
+
+    if len(best_dir) == 0:
+        raise Exception("Couldn't find android.jar")
+
+    android_jar_path = os.path.join(sdk_dir, best_dir, 'android.jar')
+
+    if not os.path.isfile(android_jar_path):
+        raise Exception("Couldn't find android.jar")
+
+    return android_jar_path
+
+def uninstall_all():
+    """Uninstall all RROs starting with 'quickrro'"""
+    packages = re.findall('quickrro[a-zA-Z.]+',
+               run_command(['adb', 'shell', 'cmd', 'overlay', 'list']))
+
+    for package in packages:
+        print('Uninstalling ' + package)
+        run_command(['adb', 'uninstall', package])
+
+    if len(packages) == 0:
+        print('No quick RROs to uninstall')
+
+def delete_arsc_flat_files(path):
+    """Deletes all .arsc.flat files under `path`"""
+    for filename in os.listdir(path):
+        if filename.endswith('.arsc.flat'):
+            run_command(['rm', os.path.join(path, filename)])
+
+def build(args, package_name):
+    """Builds the RRO apk"""
+    try:
+        android_jar_path = find_android_jar(args.I)
+    except:
+        print('Unable to find framework-res.apk / android.jar. Please build android, '
+              'install an SDK via android studio, or supply a valid -I')
+        sys.exit(1)
+
+    print('Building...')
+    root_folder = os.path.join(args.workspace, 'quick_rro')
+    manifest_file = os.path.join(root_folder, 'AndroidManifest.xml')
+    resource_folder = args.dir or os.path.join(root_folder, 'res')
+    unsigned_apk = os.path.join(root_folder, package_name + '.apk.unsigned')
+    signed_apk = os.path.join(root_folder, package_name + '.apk')
+
+    if not os.path.exists(root_folder):
+        os.makedirs(root_folder)
+
+    if args.resources is not None:
+        values_folder = os.path.join(resource_folder, 'values')
+        resource_file = os.path.join(values_folder, 'values.xml')
+
+        if not os.path.exists(values_folder):
+            os.makedirs(values_folder)
+
+        resources = map(lambda x: x.split(','), args.resources)
+        for resource in resources:
+            if len(resource) != 3:
+                print("Resource format is type,name,value")
+                sys.exit(1)
+
+        with open(resource_file, 'w') as f:
+            f.write('<?xml version="1.0" encoding="utf-8"?>\n')
+            f.write('<resources>\n')
+            for resource in resources:
+                f.write('  <item type="' + resource[0] + '" name="'
+                        + resource[1] + '">' + resource[2] + '</item>\n')
+            f.write('</resources>\n')
+
+    with open(manifest_file, 'w') as f:
+        f.write('<?xml version="1.0" encoding="utf-8"?>\n')
+        f.write('<manifest xmlns:android="http://schemas.android.com/apk/res/android"\n')
+        f.write('          package="' + package_name + '">\n')
+        f.write('    <application android:hasCode="false"/>\n')
+        f.write('    <overlay android:priority="99"\n')
+        f.write('             android:targetPackage="' + args.package + '"/>\n')
+        f.write('</manifest>\n')
+
+    run_command(['aapt2', 'compile', '-o', os.path.join(root_folder, 'compiled.zip'),
+                 '--dir', resource_folder])
+
+    delete_arsc_flat_files(root_folder)
+
+    run_command(['unzip', os.path.join(root_folder, 'compiled.zip'),
+                 '-d', root_folder])
+
+    link_command = ['aapt2', 'link', '--auto-add-overlay',
+                    '-o', unsigned_apk, '--manifest', manifest_file,
+                    '-I', android_jar_path]
+    for filename in os.listdir(root_folder):
+        if filename.endswith('.arsc.flat'):
+            link_command.extend(['-R', os.path.join(root_folder, filename)])
+    run_command(link_command)
+
+    # For some reason signapk.jar requires a relative path to out/host/linux-x86/lib64
+    os.chdir(os.environ['ANDROID_BUILD_TOP'])
+    run_command(['java', '-Djava.library.path=out/host/linux-x86/lib64',
+                 '-jar', 'out/host/linux-x86/framework/signapk.jar',
+                 'build/target/product/security/platform.x509.pem',
+                 'build/target/product/security/platform.pk8',
+                 unsigned_apk, signed_apk])
+
+    # No need to delete anything, but the unsigned apks might take a lot of space
+    try:
+        run_command(['rm', unsigned_apk])
+    except Exception:
+        pass
+
+    print('Built ' + signed_apk)
+
+def main():
+    parser = AP(description="Create and deploy a RRO (Runtime Resource Overlay)",
+                epilog='Examples:\n'
+                       '   quick_rro.py -r bool,car_ui_scrollbar_enable,false\n'
+                       '   quick_rro.py -r bool,car_ui_scrollbar_enable,false'
+                       ' -p com.android.car.ui.paintbooth\n'
+                       '   quick_rro.py -d vendor/auto/embedded/car-ui/sample1/rro/res\n'
+                       '   quick_rro.py --uninstall-all\n',
+                formatter_class=RawDescriptionHelpFormatter)
+    parser.add_argument('-r', '--resources', action='append', nargs='+',
+                        help='A resource in the form type,name,value. '
+                             'ex: -r bool,car_ui_scrollbar_enable,false')
+    parser.add_argument('-d', '--dir',
+                        help='res folder rro')
+    parser.add_argument('-p', '--package', default='com.android.car.ui.paintbooth',
+                        help='The package to override. Defaults to paintbooth.')
+    parser.add_argument('--uninstall-all', action='store_true',
+                        help='Uninstall all RROs created by this script')
+    parser.add_argument('-I',
+                        help='Path to android.jar or framework-res.apk. If not provided, will '
+                             'attempt to auto locate in $OUT/system/framework/framework-res.apk, '
+                             'and then in ~/Android/Sdk/')
+    parser.add_argument('--workspace', default='/tmp',
+                        help='The location where temporary files are made. Defaults to /tmp. '
+                             'Will make a "quickrro" folder here.')
+    args = parser.parse_args()
+
+    if args.resources is not None:
+        # flatten 2d list
+        args.resources = [x for sub in args.resources for x in sub]
+
+    if args.uninstall_all:
+        return uninstall_all()
+
+    if args.dir is None and args.resources is None:
+        print('Must include one of --resources, --dir, or --uninstall-all')
+        parser.print_help()
+        sys.exit(1)
+
+    if args.dir is not None and args.resources is not None:
+        print('Cannot specify both --resources and --dir')
+        sys.exit(1)
+
+    if not os.path.isdir(args.workspace):
+        print(str(args.workspace) + ': No such directory')
+        sys.exit(1)
+
+    if 'ANDROID_BUILD_TOP' not in os.environ:
+        print("Please run lunch first")
+        sys.exit(1)
+
+    if not os.path.isfile(os.path.join(
+            os.environ['ANDROID_BUILD_TOP'], 'out/host/linux-x86/framework/signapk.jar')):
+        print('out/host/linux-x86/framework/signapk.jar missing, please do an android build first')
+        sys.exit(1)
+
+    package_name = get_package_name(args)
+    signed_apk = os.path.join(args.workspace, 'quick_rro', package_name + '.apk')
+
+    if os.path.isfile(signed_apk):
+        print("Found cached RRO: " + signed_apk)
+    else:
+        build(args, package_name)
+
+    print('Installing...')
+    run_command(['adb', 'install', '-r', signed_apk])
+
+    print('Enabling...')
+    # Enabling RROs sometimes fails shortly after installing them
+    time.sleep(1)
+    run_command(['adb', 'shell', 'cmd', 'overlay', 'enable', '--user', '10', package_name])
+
+    print('Done!')
+
+if __name__ == "__main__":
+    main()
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();
+    }
+}
diff --git a/glide/Android.mk b/glide/Android.mk
index f8828f6..41e3937 100644
--- a/glide/Android.mk
+++ b/glide/Android.mk
@@ -14,6 +14,7 @@
 # limitations under the License.
 #
 
+LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
 LOCAL_MODULE_CLASS := JAVA_LIBRARIES