Merge "Fix bind/unbind for projection receiver app"
diff --git a/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java b/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java
index 45babf7..f93c1b4 100644
--- a/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java
+++ b/TrustAgent/src/com/android/car/trust/CarEnrolmentActivity.java
@@ -55,6 +55,7 @@
     private static final String SP_HANDLE_KEY = "sp-test";
     private static final int FINE_LOCATION_REQUEST_CODE = 42;
 
+    private PagedListView mList;
     private OutputAdapter mOutputAdapter;
     private BluetoothDevice mBluetoothDevice;
     private ICarTrustAgentBleService mCarTrustAgentBleService;
@@ -87,8 +88,8 @@
 
         mOutputAdapter = new OutputAdapter();
 
-        PagedListView list = findViewById(R.id.list);
-        list.setAdapter(mOutputAdapter);
+        mList = findViewById(R.id.list);
+        mList.setAdapter(mOutputAdapter);
     }
 
     @Override
@@ -114,7 +115,10 @@
     }
 
     private void appendOutputText(String text) {
-        runOnUiThread(() -> mOutputAdapter.addOutput(text));
+        runOnUiThread(() -> {
+            mOutputAdapter.addOutput(text);
+            mList.scrollToPosition(mOutputAdapter.getItemCount() - 1);
+        });
     }
 
     private void addEscrowToken(byte[] token) throws RemoteException {
diff --git a/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java b/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java
index 2cc2080..d70fd2f 100644
--- a/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java
+++ b/TrustAgent/src/com/android/car/trust/CarTrustAgentBleService.java
@@ -18,6 +18,8 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothGattCharacteristic;
 import android.bluetooth.BluetoothGattService;
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseSettings;
 import android.car.trust.ICarTrustAgentBleCallback;
 import android.car.trust.ICarTrustAgentBleService;
 import android.car.trust.ICarTrustAgentEnrolmentCallback;
@@ -27,12 +29,12 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.IBinder;
-import android.os.ParcelUuid;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.util.Log;
 
 import java.nio.ByteBuffer;
+import java.text.DateFormat;
 import java.util.UUID;
 
 /**
@@ -41,7 +43,6 @@
  * {@link ICarTrustAgentBleService}.
  */
 public class CarTrustAgentBleService extends SimpleBleServer {
-
     private static final String TAG = CarTrustAgentBleService.class.getSimpleName();
 
     private RemoteCallbackList<ICarTrustAgentBleCallback> mBleCallbacks;
@@ -51,15 +52,14 @@
     private ICarTrustAgentTokenResponseCallback mTokenResponseCallback;
     private CarTrustAgentBleWrapper mCarTrustBleService;
 
-    private ParcelUuid mEnrolmentUuid;
-    private BluetoothGattCharacteristic mEnrolmentEscrowToken;
-    private BluetoothGattCharacteristic mEnrolmentTokenHandle;
-    private BluetoothGattService mEnrolmentGattServer;
+    private UUID mEnrolmentEscrowTokenUuid;
+    private UUID mEnrolmentTokenHandleUuid;
+    private BluetoothGattService mEnrolmentGattService;
 
-    private ParcelUuid mUnlockUuid;
-    private BluetoothGattCharacteristic mUnlockEscrowToken;
-    private BluetoothGattCharacteristic mUnlockTokenHandle;
-    private BluetoothGattService mUnlockGattServer;
+    private UUID mUnlockEscrowTokenUuid;
+    private UUID mUnlockTokenHandleUuid;
+    private BluetoothGattService mUnlockGattService;
+
     private byte[] mCurrentUnlockToken;
     private Long mCurrentUnlockHandle;
 
@@ -73,6 +73,11 @@
         mUnlockCallbacks = new RemoteCallbackList<>();
         mCarTrustBleService = new CarTrustAgentBleWrapper();
 
+        mEnrolmentEscrowTokenUuid = UUID.fromString(getString(R.string.enrollment_token_uuid));
+        mEnrolmentTokenHandleUuid = UUID.fromString(getString(R.string.enrollment_handle_uuid));
+        mUnlockEscrowTokenUuid = UUID.fromString(getString(R.string.unlock_escrow_token_uiid));
+        mUnlockTokenHandleUuid = UUID.fromString(getString(R.string.unlock_handle_uiid));
+
         setupEnrolmentBleServer();
         setupUnlockBleServer();
 
@@ -97,8 +102,12 @@
             BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
             responseNeeded, int offset, byte[] value) {
         UUID uuid = characteristic.getUuid();
-        Log.d(TAG, "onCharacteristicWrite received uuid: " + uuid);
-        if (uuid.equals(mEnrolmentEscrowToken.getUuid())) {
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "onCharacteristicWrite received uuid: " + uuid);
+        }
+
+        if (uuid.equals(mEnrolmentEscrowTokenUuid)) {
             final int callbackCount = mEnrolmentCallbacks.beginBroadcast();
             for (int i = 0; i < callbackCount; i++) {
                 try {
@@ -108,10 +117,10 @@
                 }
             }
             mEnrolmentCallbacks.finishBroadcast();
-        } else if (uuid.equals(mUnlockEscrowToken.getUuid())) {
+        } else if (uuid.equals(mUnlockEscrowTokenUuid)) {
             mCurrentUnlockToken = value;
             maybeSendUnlockToken();
-        } else if (uuid.equals(mUnlockTokenHandle.getUuid())) {
+        } else if (uuid.equals(mUnlockTokenHandleUuid)) {
             mCurrentUnlockHandle = getLong(value);
             maybeSendUnlockToken();
         }
@@ -175,59 +184,67 @@
         mBleCallbacks.finishBroadcast();
     }
 
+    @Override
+    public void onDestroy() {
+        stopAdvertising(mEnrolmentAdvertisingCallback);
+        stopAdvertising(mUnlockAdvertisingCallback);
+        super.onDestroy();
+    }
+
     private void setupEnrolmentBleServer() {
-        mEnrolmentUuid = new ParcelUuid(
-                UUID.fromString(getString(R.string.enrollment_service_uuid)));
-        mEnrolmentGattServer = new BluetoothGattService(
+        mEnrolmentGattService = new BluetoothGattService(
                 UUID.fromString(getString(R.string.enrollment_service_uuid)),
                 BluetoothGattService.SERVICE_TYPE_PRIMARY);
 
         // Characteristic to describe the escrow token being used for unlock
-        mEnrolmentEscrowToken = new BluetoothGattCharacteristic(
-                UUID.fromString(getString(R.string.enrollment_token_uuid)),
+        BluetoothGattCharacteristic enrolmentEscrowToken = new BluetoothGattCharacteristic(
+                mEnrolmentEscrowTokenUuid,
                 BluetoothGattCharacteristic.PROPERTY_WRITE,
                 BluetoothGattCharacteristic.PERMISSION_WRITE);
 
         // Characteristic to describe the handle being used for this escrow token
-        mEnrolmentTokenHandle = new BluetoothGattCharacteristic(
-                UUID.fromString(getString(R.string.enrollment_handle_uuid)),
+        BluetoothGattCharacteristic enrolmentTokenHandle = new BluetoothGattCharacteristic(
+                mEnrolmentTokenHandleUuid,
                 BluetoothGattCharacteristic.PROPERTY_NOTIFY,
                 BluetoothGattCharacteristic.PERMISSION_READ);
 
-        mEnrolmentGattServer.addCharacteristic(mEnrolmentEscrowToken);
-        mEnrolmentGattServer.addCharacteristic(mEnrolmentTokenHandle);
+        mEnrolmentGattService.addCharacteristic(enrolmentEscrowToken);
+        mEnrolmentGattService.addCharacteristic(enrolmentTokenHandle);
     }
 
     private void setupUnlockBleServer() {
-        mUnlockUuid = new ParcelUuid(
-                UUID.fromString(getString(R.string.unlock_service_uuid)));
-        mUnlockGattServer = new BluetoothGattService(
+        mUnlockGattService = new BluetoothGattService(
                 UUID.fromString(getString(R.string.unlock_service_uuid)),
                 BluetoothGattService.SERVICE_TYPE_PRIMARY);
 
         // Characteristic to describe the escrow token being used for unlock
-        mUnlockEscrowToken = new BluetoothGattCharacteristic(
-                UUID.fromString(getString(R.string.unlock_escrow_token_uiid)),
+        BluetoothGattCharacteristic unlockEscrowToken = new BluetoothGattCharacteristic(
+                mUnlockEscrowTokenUuid,
                 BluetoothGattCharacteristic.PROPERTY_WRITE,
                 BluetoothGattCharacteristic.PERMISSION_WRITE);
 
         // Characteristic to describe the handle being used for this escrow token
-        mUnlockTokenHandle = new BluetoothGattCharacteristic(
-                UUID.fromString(getString(R.string.unlock_handle_uiid)),
+        BluetoothGattCharacteristic unlockTokenHandle = new BluetoothGattCharacteristic(
+                mUnlockTokenHandleUuid,
                 BluetoothGattCharacteristic.PROPERTY_WRITE,
                 BluetoothGattCharacteristic.PERMISSION_WRITE);
 
-        mUnlockGattServer.addCharacteristic(mUnlockEscrowToken);
-        mUnlockGattServer.addCharacteristic(mUnlockTokenHandle);
+        mUnlockGattService.addCharacteristic(unlockEscrowToken);
+        mUnlockGattService.addCharacteristic(unlockTokenHandle);
     }
 
     private synchronized void maybeSendUnlockToken() {
         if (mCurrentUnlockToken == null || mCurrentUnlockHandle == null) {
             return;
         }
-        Log.d(TAG, "Handle and token both received, requesting unlock. Time: "
-                + System.currentTimeMillis());
-        final int callbackCount = mUnlockCallbacks.beginBroadcast();
+
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "Handle and token both received, requesting unlock. Time: "
+                    + DateFormat.getDateInstance(DateFormat.SHORT).format(
+                            System.currentTimeMillis()));
+        }
+
+        int callbackCount = mUnlockCallbacks.beginBroadcast();
         for (int i = 0; i < callbackCount; i++) {
             try {
                 mUnlockCallbacks.getBroadcastItem(i).onUnlockDataReceived(
@@ -254,6 +271,46 @@
         return buffer.getLong();
     }
 
+    private final AdvertiseCallback mEnrolmentAdvertisingCallback = new AdvertiseCallback() {
+        @Override
+        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+            super.onStartSuccess(settingsInEffect);
+            onAdvertiseStartSuccess();
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Successfully started advertising service");
+            }
+        }
+
+        @Override
+        public void onStartFailure(int errorCode) {
+            Log.e(TAG, "Failed to advertise, errorCode: " + errorCode);
+
+            super.onStartFailure(errorCode);
+            onAdvertiseStartFailure(errorCode);
+        }
+    };
+
+    private final AdvertiseCallback mUnlockAdvertisingCallback = new AdvertiseCallback() {
+        @Override
+        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+            super.onStartSuccess(settingsInEffect);
+            onAdvertiseStartSuccess();
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Successfully started advertising service");
+            }
+        }
+
+        @Override
+        public void onStartFailure(int errorCode) {
+            Log.e(TAG, "Failed to advertise, errorCode: " + errorCode);
+
+            super.onStartFailure(errorCode);
+            onAdvertiseStartFailure(errorCode);
+        }
+    };
+
     private final class CarTrustAgentBleWrapper extends ICarTrustAgentBleService.Stub {
         @Override
         public void registerBleCallback(ICarTrustAgentBleCallback callback) {
@@ -267,22 +324,33 @@
 
         @Override
         public void startEnrolmentAdvertising() {
+            stopEnrolmentAdvertising();
             stopUnlockAdvertising();
-            Log.d(TAG, "startEnrolmentAdvertising");
-            startAdvertising(mEnrolmentUuid, mEnrolmentGattServer);
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "startEnrolmentAdvertising");
+            }
+            startAdvertising(mEnrolmentGattService, mEnrolmentAdvertisingCallback);
         }
 
         @Override
         public void stopEnrolmentAdvertising() {
-            Log.d(TAG, "stopEnrolmentAdvertising");
-            stopAdvertising();
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "stopEnrolmentAdvertising");
+            }
+
+            stopAdvertising(mEnrolmentAdvertisingCallback);
         }
 
         @Override
         public void sendEnrolmentHandle(BluetoothDevice device, long handle) {
-            Log.d(TAG, "sendEnrolmentHandle: " + handle);
-            mEnrolmentTokenHandle.setValue(getBytes(handle));
-            notifyCharacteristicChanged(device, mEnrolmentTokenHandle, false);
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "sendEnrolmentHandle: " + handle);
+            }
+            BluetoothGattCharacteristic enrolmentTokenHandle =
+                    mEnrolmentGattService.getCharacteristic(mEnrolmentTokenHandleUuid);
+            enrolmentTokenHandle.setValue(getBytes(handle));
+            notifyCharacteristicChanged(device, enrolmentTokenHandle, false);
         }
 
         @Override
@@ -297,15 +365,21 @@
 
         @Override
         public void startUnlockAdvertising() {
+            stopUnlockAdvertising();
             stopEnrolmentAdvertising();
-            Log.d(TAG, "startUnlockAdvertising");
-            startAdvertising(mUnlockUuid, mUnlockGattServer);
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "startUnlockAdvertising");
+            }
+            startAdvertising(mUnlockGattService, mUnlockAdvertisingCallback);
         }
 
         @Override
         public void stopUnlockAdvertising() {
-            Log.d(TAG, "stopUnlockAdvertising");
-            stopAdvertising();
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "stopUnlockAdvertising");
+            }
+            stopAdvertising(mUnlockAdvertisingCallback);
         }
 
         @Override
@@ -359,7 +433,10 @@
         @Override
         public void onEscrowTokenAdded(byte[] token, long handle, int uid)
                 throws RemoteException {
-            Log.d(TAG, "onEscrowTokenAdded handle:" + handle + " uid:" + uid);
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "onEscrowTokenAdded handle:" + handle + " uid:" + uid);
+            }
+
             mTokenHandleSharedPreferences.edit()
                     .putInt(String.valueOf(handle), uid)
                     .apply();
@@ -370,7 +447,10 @@
 
         @Override
         public void onEscrowTokenRemoved(long handle, boolean successful) throws RemoteException {
-            Log.d(TAG, "onEscrowTokenRemoved handle:" + handle);
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "onEscrowTokenRemoved handle:" + handle);
+            }
+
             mTokenHandleSharedPreferences.edit()
                     .remove(String.valueOf(handle))
                     .apply();
diff --git a/TrustAgent/src/com/android/car/trust/SimpleBleServer.java b/TrustAgent/src/com/android/car/trust/SimpleBleServer.java
index 7fbf2fe..260f5ac 100644
--- a/TrustAgent/src/com/android/car/trust/SimpleBleServer.java
+++ b/TrustAgent/src/com/android/car/trust/SimpleBleServer.java
@@ -41,72 +41,11 @@
  * A generic service to start a BLE
  */
 public abstract class SimpleBleServer extends Service {
-
     private static final String TAG = SimpleBleServer.class.getSimpleName();
 
     private static final int BLE_RETRY_LIMIT = 5;
     private static final int BLE_RETRY_INTERVAL_MS = 1000;
 
-    private final AdvertiseCallback mAdvertisingCallback = new AdvertiseCallback() {
-        @Override
-        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
-            super.onStartSuccess(settingsInEffect);
-            Log.d(TAG, "Successfully started advertising service");
-            onAdvertiseStartSuccess();
-        }
-
-        @Override
-        public void onStartFailure(int errorCode) {
-            super.onStartFailure(errorCode);
-            Log.e(TAG, "Failed to advertise, errorCode: " + errorCode);
-            onAdvertiseStartFailure(errorCode);
-        }
-    };
-
-    private final BluetoothGattServerCallback mGattServerCallback =
-            new BluetoothGattServerCallback() {
-        @Override
-        public void onConnectionStateChange(BluetoothDevice device,
-                final int status, final int newState) {
-            Log.d(TAG, "GattServer connection change status: " + status
-                    + " newState: " + newState
-                    + " device name: " + device.getName());
-            switch (newState) {
-                case BluetoothProfile.STATE_CONNECTED:
-                    onAdvertiseDeviceConnected(device);
-                    break;
-                case BluetoothProfile.STATE_DISCONNECTED:
-                    onAdvertiseDeviceDisconnected(device);
-                    break;
-            }
-        }
-
-        @Override
-        public void onServiceAdded(final int status, BluetoothGattService service) {
-            Log.d(TAG, "Service added status: " + status + " uuid: " + service.getUuid());
-        }
-
-        @Override
-        public void onCharacteristicReadRequest(BluetoothDevice device,
-                int requestId, int offset, final BluetoothGattCharacteristic characteristic) {
-            Log.d(TAG, "Read request for characteristic: " + characteristic.getUuid());
-            mGattServer.sendResponse(device, requestId,
-                    BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
-            onCharacteristicRead(device, requestId, offset, characteristic);
-        }
-
-        @Override
-        public void onCharacteristicWriteRequest(final BluetoothDevice device, int requestId,
-                BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
-                responseNeeded, int offset, byte[] value) {
-            Log.d(TAG, "Write request for characteristic: " + characteristic.getUuid());
-            mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
-                    offset, value);
-            onCharacteristicWrite(device, requestId, characteristic,
-                    preparedWrite, responseNeeded, offset, value);
-        }
-    };
-
     private final Handler mHandler = new Handler();
 
     private BluetoothManager mBluetoothManager;
@@ -116,29 +55,32 @@
 
     /**
      * Starts the GATT server with the given {@link BluetoothGattService} and begins
-     * advertising with the {@link ParcelUuid}.
+     * 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 advertiseUuid Service Uuid used in the {@link AdvertiseData}
      * @param service {@link BluetoothGattService} that will be discovered by clients
      */
-    protected void startAdvertising(ParcelUuid advertiseUuid, BluetoothGattService service) {
+    protected void startAdvertising(BluetoothGattService service,
+            AdvertiseCallback advertiseCallback) {
         if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
             Log.e(TAG, "System does not support BLE");
             return;
         }
 
-        mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
-        mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
+        // Only open one Gatt server.
         if (mGattServer == null) {
-            Log.e(TAG, "Gatt Server not created");
-            return;
+            mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
+            mGattServer = mBluetoothManager.openGattServer(this, mGattServerCallback);
+
+            if (mGattServer == null) {
+                Log.e(TAG, "Gatt Server not created");
+                return;
+            }
         }
 
-        // We only allow adding one service in this implementation. If multiple services need
-        // to be added, then they need to be queued up and added only after
-        // BluetoothGattServerCallback.onServiceAdded is called.
+        mGattServer.clearServices();
         mGattServer.addService(service);
 
         AdvertiseSettings settings = new AdvertiseSettings.Builder()
@@ -149,29 +91,31 @@
 
         AdvertiseData data = new AdvertiseData.Builder()
                 .setIncludeDeviceName(true)
-                .addServiceUuid(advertiseUuid)
+                .addServiceUuid(new ParcelUuid(service.getUuid()))
                 .build();
 
         mAdvertiserStartCount = 0;
-        startAdvertisingInternally(settings, data);
+        startAdvertisingInternally(settings, data, advertiseCallback);
     }
 
-    private void startAdvertisingInternally(AdvertiseSettings settings, AdvertiseData data) {
+    private void startAdvertisingInternally(AdvertiseSettings settings, AdvertiseData data,
+            AdvertiseCallback advertiseCallback) {
         mAdvertiserStartCount += 1;
         mAdvertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
         if (mAdvertiser == null && mAdvertiserStartCount < BLE_RETRY_LIMIT) {
-            mHandler.postDelayed(() -> startAdvertisingInternally(settings, data),
-                    BLE_RETRY_INTERVAL_MS);
+            mHandler.postDelayed(
+                    () -> startAdvertisingInternally(settings, data, advertiseCallback),
+                            BLE_RETRY_INTERVAL_MS);
         } else {
             mHandler.removeCallbacks(null);
-            mAdvertiser.startAdvertising(settings, data, mAdvertisingCallback);
+            mAdvertiser.startAdvertising(settings, data, advertiseCallback);
             mAdvertiserStartCount = 0;
         }
     }
 
-    protected void stopAdvertising() {
+    protected void stopAdvertising(AdvertiseCallback advertiseCallback) {
         if (mAdvertiser != null) {
-            mAdvertiser.stopAdvertising(mAdvertisingCallback);
+            mAdvertiser.stopAdvertising(advertiseCallback);
         }
     }
 
@@ -189,7 +133,6 @@
     public void onDestroy() {
         // Stops the advertiser and GATT server. This needs to be done to avoid leaks
         if (mAdvertiser != null) {
-            mAdvertiser.stopAdvertising(mAdvertisingCallback);
             mAdvertiser.cleanup();
         }
 
@@ -228,4 +171,55 @@
     protected abstract void onCharacteristicRead(BluetoothDevice device,
             int requestId, int offset, final BluetoothGattCharacteristic characteristic);
 
+    private final BluetoothGattServerCallback mGattServerCallback =
+            new BluetoothGattServerCallback() {
+        @Override
+        public void onConnectionStateChange(BluetoothDevice device,
+                final int status, final int newState) {
+            switch (newState) {
+                case BluetoothProfile.STATE_CONNECTED:
+                    onAdvertiseDeviceConnected(device);
+                    break;
+                case BluetoothProfile.STATE_DISCONNECTED:
+                    onAdvertiseDeviceDisconnected(device);
+                    break;
+                default:
+                    Log.w(TAG, "Connection state not connecting or disconnecting; ignoring: "
+                            + newState);
+            }
+        }
+
+        @Override
+        public void onServiceAdded(final int status, BluetoothGattService service) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Service added status: " + status + " uuid: " + service.getUuid());
+            }
+        }
+
+        @Override
+        public void onCharacteristicReadRequest(BluetoothDevice device,
+                int requestId, int offset, final BluetoothGattCharacteristic characteristic) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Read request for characteristic: " + characteristic.getUuid());
+            }
+
+            mGattServer.sendResponse(device, requestId,
+                    BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
+            onCharacteristicRead(device, requestId, offset, characteristic);
+        }
+
+        @Override
+        public void onCharacteristicWriteRequest(final BluetoothDevice device, int requestId,
+                BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean
+                responseNeeded, int offset, byte[] value) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Write request for characteristic: " + characteristic.getUuid());
+            }
+
+            mGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,
+                    offset, value);
+            onCharacteristicWrite(device, requestId, characteristic,
+                    preparedWrite, responseNeeded, offset, value);
+        }
+    };
 }
diff --git a/car-lib/api/current.txt b/car-lib/api/current.txt
index 6744238..4d000ee 100644
--- a/car-lib/api/current.txt
+++ b/car-lib/api/current.txt
@@ -447,20 +447,31 @@
     method public android.car.media.CarAudioPatchHandle createAudioPatch(java.lang.String, int, int) throws android.car.CarNotConnectedException;
     method public java.lang.String[] getExternalSources() throws android.car.CarNotConnectedException;
     method public int getGroupMaxVolume(int) throws android.car.CarNotConnectedException;
+    method public int getGroupMaxVolume(int, int) throws android.car.CarNotConnectedException;
     method public int getGroupMinVolume(int) throws android.car.CarNotConnectedException;
+    method public int getGroupMinVolume(int, int) throws android.car.CarNotConnectedException;
     method public int getGroupVolume(int) throws android.car.CarNotConnectedException;
+    method public int getGroupVolume(int, int) throws android.car.CarNotConnectedException;
     method public int[] getUsagesForVolumeGroupId(int) throws android.car.CarNotConnectedException;
+    method public int[] getUsagesForVolumeGroupId(int, int) throws android.car.CarNotConnectedException;
     method public int getVolumeGroupCount() throws android.car.CarNotConnectedException;
+    method public int getVolumeGroupCount(int) throws android.car.CarNotConnectedException;
     method public int getVolumeGroupIdForUsage(int) throws android.car.CarNotConnectedException;
-    method public static java.lang.String getVolumeSettingsKeyForGroup(int);
-    method public void registerVolumeCallback(android.os.IBinder) throws android.car.CarNotConnectedException;
-    method public void registerVolumeChangeObserver(android.database.ContentObserver);
+    method public int getVolumeGroupIdForUsage(int, int) throws android.car.CarNotConnectedException;
+    method public void registerCarVolumeCallback(android.car.media.CarAudioManager.CarVolumeCallback);
     method public void releaseAudioPatch(android.car.media.CarAudioPatchHandle) throws android.car.CarNotConnectedException;
     method public void setBalanceTowardRight(float) throws android.car.CarNotConnectedException;
     method public void setFadeTowardFront(float) throws android.car.CarNotConnectedException;
     method public void setGroupVolume(int, int, int) throws android.car.CarNotConnectedException;
-    method public void unregisterVolumeCallback(android.os.IBinder) throws android.car.CarNotConnectedException;
-    method public void unregisterVolumeChangeObserver(android.database.ContentObserver);
+    method public void setGroupVolume(int, int, int, int) throws android.car.CarNotConnectedException;
+    method public void unregisterCarVolumeCallback(android.car.media.CarAudioManager.CarVolumeCallback);
+    field public static final int PRIMARY_AUDIO_ZONE = 0; // 0x0
+  }
+
+  public static abstract class CarAudioManager.CarVolumeCallback {
+    ctor public CarAudioManager.CarVolumeCallback();
+    method public void onGroupVolumeChanged(int, int, int);
+    method public void onMasterMuteChanged(int, int);
   }
 
   public final class CarAudioPatchHandle implements android.os.Parcelable {
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index fcbe439..a0046eb 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -734,29 +734,6 @@
 
 }
 
-package android.car.media {
-
-  public final class CarAudioManager {
-    method public android.car.media.CarAudioPatchHandle createAudioPatch(java.lang.String, int, int) throws android.car.CarNotConnectedException;
-    method public java.lang.String[] getExternalSources() throws android.car.CarNotConnectedException;
-    method public int getGroupMaxVolume(int) throws android.car.CarNotConnectedException;
-    method public int getGroupMinVolume(int) throws android.car.CarNotConnectedException;
-    method public int getGroupVolume(int) throws android.car.CarNotConnectedException;
-    method public int[] getUsagesForVolumeGroupId(int) throws android.car.CarNotConnectedException;
-    method public int getVolumeGroupCount() throws android.car.CarNotConnectedException;
-    method public int getVolumeGroupIdForUsage(int) throws android.car.CarNotConnectedException;
-    method public void registerVolumeCallback(android.os.IBinder) throws android.car.CarNotConnectedException;
-    method public void registerVolumeChangeObserver(android.database.ContentObserver);
-    method public void releaseAudioPatch(android.car.media.CarAudioPatchHandle) throws android.car.CarNotConnectedException;
-    method public void setBalanceTowardRight(float) throws android.car.CarNotConnectedException;
-    method public void setFadeTowardFront(float) throws android.car.CarNotConnectedException;
-    method public void setGroupVolume(int, int, int) throws android.car.CarNotConnectedException;
-    method public void unregisterVolumeCallback(android.os.IBinder) throws android.car.CarNotConnectedException;
-    method public void unregisterVolumeChangeObserver(android.database.ContentObserver);
-  }
-
-}
-
 package android.car.navigation {
 
   public class CarNavigationInstrumentCluster implements android.os.Parcelable {
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index c1bf502..e262e91 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -405,7 +405,16 @@
      * @hide
      */
     @SystemApi
-    public static final String PERMISSION_CAR_DIAGNOSTIC_CLEAR = "android.car.permission.CLEAR_CAR_DIAGNOSTICS";
+    public static final String PERMISSION_CAR_DIAGNOSTIC_CLEAR =
+            "android.car.permission.CLEAR_CAR_DIAGNOSTICS";
+
+    /**
+     * Permission necessary to configure UX restrictions through {@link CarUxRestrictionsManager}.
+     *
+     * @hide
+     */
+    public static final String PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION =
+            "android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION";
 
     /**
      * Permissions necessary to clear diagnostic information.
@@ -413,7 +422,8 @@
      * @hide
      */
     @SystemApi
-    public static final String PERMISSION_STORAGE_MONITORING = "android.car.permission.STORAGE_MONITORING";
+    public static final String PERMISSION_STORAGE_MONITORING =
+            "android.car.permission.STORAGE_MONITORING";
 
     /** Type of car connection: platform runs directly in car. */
     public static final int CONNECTION_TYPE_EMBEDDED = 5;
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/colors.xml b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.aidl
similarity index 67%
rename from tests/EmbeddedKitchenSinkApp/res/values/colors.xml
rename to car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.aidl
index 9a1ed89..e6f85b5 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/colors.xml
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.aidl
@@ -1,20 +1,19 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- * Copyright (c) 2017, The Android Open Source Project
+/*
+ * 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
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT 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>
-    <color name="car_button_tint">#fffafafa</color>
-</resources>
\ No newline at end of file
+ */
+
+package android.car.drivingstate;
+
+parcelable CarUxRestrictionsConfiguration;
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java
new file mode 100644
index 0000000..06d19df
--- /dev/null
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsConfiguration.java
@@ -0,0 +1,732 @@
+/*
+ * 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 android.car.drivingstate;
+
+import android.annotation.FloatRange;
+import android.annotation.Nullable;
+import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Configuration for Car UX Restrictions service.
+ *
+ * @hide
+ */
+public final class CarUxRestrictionsConfiguration implements Parcelable {
+    private static final String TAG = "CarUxRConfig";
+
+    // Constants used by json de/serialization.
+    private static final String JSON_NAME_MAX_CONTENT_DEPTH = "max_content_depth";
+    private static final String JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS =
+            "max_cumulative_content_items";
+    private static final String JSON_NAME_MAX_STRING_LENGTH = "max_string_length";
+    private static final String JSON_NAME_MOVING_RESTRICTIONS = "moving_restrictions";
+    private static final String JSON_NAME_IDLING_RESTRICTIONS = "idling_restrictions";
+    private static final String JSON_NAME_PARKED_RESTRICTIONS = "parked_restrictions";
+    private static final String JSON_NAME_UNKNOWN_RESTRICTIONS = "unknown_restrictions";
+    private static final String JSON_NAME_REQ_OPT = "req_opt";
+    private static final String JSON_NAME_RESTRICTIONS = "restrictions";
+    private static final String JSON_NAME_SPEED_RANGE = "speed_range";
+    private static final String JSON_NAME_MIN_SPEED = "min_speed";
+    private static final String JSON_NAME_MAX_SPEED = "max_speed";
+
+    private final int mMaxContentDepth;
+    private final int mMaxCumulativeContentItems;
+    private final int mMaxStringLength;
+    private final Map<Integer, List<RestrictionsPerSpeedRange>> mUxRestrictions =
+            new ArrayMap<>(DRIVING_STATES.length);
+
+    private CarUxRestrictionsConfiguration(CarUxRestrictionsConfiguration.Builder builder) {
+        mMaxContentDepth = builder.mMaxContentDepth;
+        mMaxCumulativeContentItems = builder.mMaxCumulativeContentItems;
+        mMaxStringLength = builder.mMaxStringLength;
+
+        for (int drivingState : DRIVING_STATES) {
+            List<RestrictionsPerSpeedRange> list = new ArrayList<>();
+            for (RestrictionsPerSpeedRange r : builder.mUxRestrictions.get(drivingState)) {
+                list.add(r);
+            }
+            mUxRestrictions.put(drivingState, list);
+        }
+    }
+
+    /**
+     * Returns the restrictions based on current driving state and speed.
+     */
+    public CarUxRestrictions getUxRestrictions(@CarDrivingState int drivingState,
+            float currentSpeed) {
+        List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
+        if (restrictions.isEmpty()) {
+            if (Build.IS_ENG || Build.IS_USERDEBUG) {
+                throw new IllegalStateException("No restrictions for driving state "
+                        + getDrivingStateName(drivingState));
+            }
+            return createDefaultUxRestrictionsEvent();
+        }
+
+        RestrictionsPerSpeedRange restriction = null;
+        if (restrictions.size() == 1) {
+            restriction = restrictions.get(0);
+        } else {
+            for (RestrictionsPerSpeedRange r : restrictions) {
+                if (r.mSpeedRange != null && r.mSpeedRange.includes(currentSpeed)) {
+                    restriction = r;
+                    break;
+                }
+            }
+        }
+
+        if (restriction == null) {
+            if (Build.IS_ENG || Build.IS_USERDEBUG) {
+                throw new IllegalStateException(
+                        "No restrictions found for driving state " + drivingState
+                                + " at speed " + currentSpeed);
+            }
+            return createDefaultUxRestrictionsEvent();
+        }
+        return createUxRestrictionsEvent(restriction.mReqOpt, restriction.mRestrictions);
+    }
+
+    private CarUxRestrictions createDefaultUxRestrictionsEvent() {
+        return createUxRestrictionsEvent(true,
+                CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED);
+    }
+
+    /**
+     * Creates CarUxRestrictions with restrictions parameters from current configuration.
+     */
+    private CarUxRestrictions createUxRestrictionsEvent(boolean requiresOpt,
+            @CarUxRestrictions.CarUxRestrictionsInfo int uxr) {
+        // In case the UXR is not baseline, set requiresDistractionOptimization to true since it
+        // doesn't make sense to have an active non baseline restrictions without
+        // requiresDistractionOptimization set to true.
+        if (uxr != CarUxRestrictions.UX_RESTRICTIONS_BASELINE) {
+            requiresOpt = true;
+        }
+        CarUxRestrictions.Builder builder = new CarUxRestrictions.Builder(requiresOpt, uxr,
+                SystemClock.elapsedRealtimeNanos());
+        if (mMaxStringLength != Builder.UX_RESTRICTIONS_UNKNOWN) {
+            builder.setMaxStringLength(mMaxStringLength);
+        }
+        if (mMaxCumulativeContentItems != Builder.UX_RESTRICTIONS_UNKNOWN) {
+            builder.setMaxCumulativeContentItems(mMaxCumulativeContentItems);
+        }
+        if (mMaxContentDepth != Builder.UX_RESTRICTIONS_UNKNOWN) {
+            builder.setMaxContentDepth(mMaxContentDepth);
+        }
+        return builder.build();
+    }
+
+    // Json de/serialization methods.
+
+    /**
+     * Writes current configuration as Json.
+     */
+    public void writeJson(JsonWriter writer) throws IOException {
+        // We need to be lenient to accept infinity number (as max speed).
+        writer.setLenient(true);
+
+        writer.beginObject();
+
+        writer.name(JSON_NAME_MAX_CONTENT_DEPTH).value(mMaxContentDepth);
+        writer.name(JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS).value(
+                mMaxCumulativeContentItems);
+        writer.name(JSON_NAME_MAX_STRING_LENGTH).value(mMaxStringLength);
+
+        writer.name(JSON_NAME_PARKED_RESTRICTIONS);
+        writeRestrictionsList(writer,
+                mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_PARKED));
+
+        writer.name(JSON_NAME_IDLING_RESTRICTIONS);
+        writeRestrictionsList(writer,
+                mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_IDLING));
+
+        writer.name(JSON_NAME_MOVING_RESTRICTIONS);
+        writeRestrictionsList(writer,
+                mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_MOVING));
+
+        writer.name(JSON_NAME_UNKNOWN_RESTRICTIONS);
+        writeRestrictionsList(writer,
+                mUxRestrictions.get(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN));
+
+        writer.endObject();
+    }
+
+    private void writeRestrictionsList(JsonWriter writer, List<RestrictionsPerSpeedRange> messages)
+            throws IOException {
+        writer.beginArray();
+        for (RestrictionsPerSpeedRange restrictions : messages) {
+            writeRestrictions(writer, restrictions);
+        }
+        writer.endArray();
+    }
+
+    private void writeRestrictions(JsonWriter writer, RestrictionsPerSpeedRange restrictions)
+            throws IOException {
+        writer.beginObject();
+        writer.name(JSON_NAME_REQ_OPT).value(restrictions.mReqOpt);
+        writer.name(JSON_NAME_RESTRICTIONS).value(restrictions.mRestrictions);
+        if (restrictions.mSpeedRange != null) {
+            writer.name(JSON_NAME_SPEED_RANGE);
+            writer.beginObject();
+            writer.name(JSON_NAME_MIN_SPEED).value(restrictions.mSpeedRange.mMinSpeed);
+            writer.name(JSON_NAME_MAX_SPEED).value(restrictions.mSpeedRange.mMaxSpeed);
+            writer.endObject();
+        }
+        writer.endObject();
+    }
+
+    /**
+     * Reads Json as UX restriction configuration.
+     */
+    public static CarUxRestrictionsConfiguration readJson(JsonReader reader) throws IOException {
+        // We need to be lenient to accept infinity number (as max speed).
+        reader.setLenient(true);
+
+        Builder builder = new Builder();
+        reader.beginObject();
+        while (reader.hasNext()) {
+            String name = reader.nextName();
+            if (name.equals(JSON_NAME_MAX_CONTENT_DEPTH)) {
+                builder.setMaxContentDepth(reader.nextInt());
+            } else if (name.equals(JSON_NAME_MAX_CUMULATIVE_CONTENT_ITEMS)) {
+                builder.setMaxCumulativeContentItems(reader.nextInt());
+            } else if (name.equals(JSON_NAME_MAX_STRING_LENGTH)) {
+                builder.setMaxStringLength(reader.nextInt());
+            } else if (name.equals(JSON_NAME_PARKED_RESTRICTIONS)) {
+                readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_PARKED, builder);
+            } else if (name.equals(JSON_NAME_IDLING_RESTRICTIONS)) {
+                readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_IDLING, builder);
+            } else if (name.equals(JSON_NAME_MOVING_RESTRICTIONS)) {
+                readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_MOVING, builder);
+            } else if (name.equals(JSON_NAME_UNKNOWN_RESTRICTIONS)) {
+                readRestrictionsList(reader, CarDrivingStateEvent.DRIVING_STATE_UNKNOWN, builder);
+            } else {
+                Log.e(TAG, "Unknown name parsing json config: " + name);
+                reader.skipValue();
+            }
+        }
+        reader.endObject();
+        return builder.build();
+    }
+
+    private static void readRestrictionsList(JsonReader reader, @CarDrivingState int drivingState,
+            Builder builder) throws IOException {
+        reader.beginArray();
+        while (reader.hasNext()) {
+            readRestrictions(reader, drivingState, builder);
+        }
+        reader.endArray();
+    }
+
+    private static void readRestrictions(JsonReader reader, @CarDrivingState int drivingState,
+            Builder builder) throws IOException {
+        reader.beginObject();
+        boolean reqOpt = false;
+        int restrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
+        Builder.SpeedRange speedRange = null;
+        while (reader.hasNext()) {
+            String name = reader.nextName();
+            if (name.equals(JSON_NAME_REQ_OPT)) {
+                reqOpt = reader.nextBoolean();
+            } else if (name.equals(JSON_NAME_RESTRICTIONS)) {
+                restrictions = reader.nextInt();
+            } else if (name.equals(JSON_NAME_SPEED_RANGE)) {
+                reader.beginObject();
+                // Okay to set min initial value as MAX_SPEED because SpeedRange() won't allow it.
+                float minSpeed = Builder.SpeedRange.MAX_SPEED;
+                float maxSpeed = Builder.SpeedRange.MAX_SPEED;
+
+                while (reader.hasNext()) {
+                    String n = reader.nextName();
+                    if (n.equals(JSON_NAME_MIN_SPEED)) {
+                        minSpeed = Double.valueOf(reader.nextDouble()).floatValue();
+                    } else if (n.equals(JSON_NAME_MAX_SPEED)) {
+                        maxSpeed = Double.valueOf(reader.nextDouble()).floatValue();
+                    } else {
+                        Log.e(TAG, "Unknown name parsing json config: " + n);
+                        reader.skipValue();
+                    }
+                }
+                speedRange = new Builder.SpeedRange(minSpeed, maxSpeed);
+                reader.endObject();
+            }
+        }
+        reader.endObject();
+        builder.setUxRestrictions(drivingState, speedRange, reqOpt, restrictions);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || !(obj instanceof CarUxRestrictionsConfiguration)) {
+            return false;
+        }
+
+        CarUxRestrictionsConfiguration other = (CarUxRestrictionsConfiguration) obj;
+
+        // Compare UXR parameters.
+        if (mMaxContentDepth != other.mMaxContentDepth
+                || mMaxCumulativeContentItems != other.mMaxCumulativeContentItems
+                || mMaxStringLength != other.mMaxStringLength) {
+            return false;
+        }
+
+        // Compare UXR by driving state.
+        if (!mUxRestrictions.keySet().equals(other.mUxRestrictions.keySet())) {
+            return false;
+        }
+        for (int drivingState : mUxRestrictions.keySet()) {
+            List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(
+                    drivingState);
+            List<RestrictionsPerSpeedRange> otherRestrictions = other.mUxRestrictions.get(
+                    drivingState);
+            if (restrictions.size() != otherRestrictions.size()) {
+                return false;
+            }
+            // Assuming the restrictions are sorted.
+            for (int i = 0; i < restrictions.size(); i++) {
+                if (!restrictions.get(i).equals(otherRestrictions.get(i))) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Dump the driving state to UX restrictions mapping.
+     */
+    public void dump(PrintWriter writer) {
+        for (Integer state : mUxRestrictions.keySet()) {
+            List<RestrictionsPerSpeedRange> list = mUxRestrictions.get(state);
+            writer.println("===========================================");
+            writer.println("Driving State to UXR");
+            if (list != null) {
+                writer.println("State:" + getDrivingStateName(state) + " num restrictions:"
+                        + list.size());
+                for (RestrictionsPerSpeedRange r : list) {
+                    writer.println("Requires DO? " + r.mReqOpt
+                            + "\nRestrictions: 0x" + Integer.toHexString(r.mRestrictions)
+                            + "\nSpeed Range: " + (r.mSpeedRange == null
+                            ? "None"
+                            : r.mSpeedRange.mMinSpeed + " - " + r.mSpeedRange.mMaxSpeed));
+                    writer.println("===========================================");
+                }
+            }
+        }
+        writer.println("Max String length: " + mMaxStringLength);
+        writer.println("Max Cumulative Content Items: " + mMaxCumulativeContentItems);
+        writer.println("Max Content depth: " + mMaxContentDepth);
+    }
+
+    private static String getDrivingStateName(@CarDrivingState int state) {
+        switch (state) {
+            case 0:
+                return "parked";
+            case 1:
+                return "idling";
+            case 2:
+                return "moving";
+            default:
+                return "unknown";
+        }
+    }
+
+    // Parcelable methods/fields.
+
+    // Used by Parcel methods to ensure de/serialization order.
+    private static final int[] DRIVING_STATES = new int[]{
+            CarDrivingStateEvent.DRIVING_STATE_UNKNOWN,
+            CarDrivingStateEvent.DRIVING_STATE_PARKED,
+            CarDrivingStateEvent.DRIVING_STATE_IDLING,
+            CarDrivingStateEvent.DRIVING_STATE_MOVING
+    };
+
+    public static final Parcelable.Creator<CarUxRestrictionsConfiguration> CREATOR =
+            new Parcelable.Creator<CarUxRestrictionsConfiguration>() {
+
+        @Override
+        public CarUxRestrictionsConfiguration createFromParcel(Parcel source) {
+            return new CarUxRestrictionsConfiguration(source);
+        }
+
+        @Override
+        public CarUxRestrictionsConfiguration[] newArray(int size) {
+            return new CarUxRestrictionsConfiguration[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    private CarUxRestrictionsConfiguration(Parcel in) {
+        for (int drivingState : DRIVING_STATES) {
+            List<RestrictionsPerSpeedRange> restrictions = new ArrayList<>();
+            in.readTypedList(restrictions, RestrictionsPerSpeedRange.CREATOR);
+            mUxRestrictions.put(drivingState, restrictions);
+        }
+        mMaxContentDepth = in.readInt();
+        mMaxCumulativeContentItems = in.readInt();
+        mMaxStringLength = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        for (int drivingState : DRIVING_STATES) {
+            dest.writeTypedList(mUxRestrictions.get(drivingState), 0);
+        }
+        dest.writeInt(mMaxContentDepth);
+        dest.writeInt(mMaxCumulativeContentItems);
+        dest.writeInt(mMaxStringLength);
+    }
+
+    /**
+     * @hide
+     */
+    public static final class Builder {
+
+        private static final int UX_RESTRICTIONS_UNKNOWN = -1;
+
+        private int mMaxContentDepth = UX_RESTRICTIONS_UNKNOWN;
+        private int mMaxCumulativeContentItems = UX_RESTRICTIONS_UNKNOWN;
+        private int mMaxStringLength = UX_RESTRICTIONS_UNKNOWN;
+
+        private Map<Integer, List<RestrictionsPerSpeedRange>> mUxRestrictions =
+                new ArrayMap<>(DRIVING_STATES.length);
+
+        public Builder() {
+            for (int drivingState : DRIVING_STATES) {
+                mUxRestrictions.put(drivingState, new ArrayList<>());
+            }
+        }
+
+        /**
+         * Sets ux restrictions for driving state.
+         */
+        public Builder setUxRestrictions(@CarDrivingState int drivingState,
+                boolean requiresOptimization,
+                @CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
+            return this.setUxRestrictions(drivingState, null, requiresOptimization,  restrictions);
+        }
+
+        /**
+         * Sets ux restrictions with speed range.
+         *
+         * @param drivingState Restrictions will be set for this Driving state.
+         *                     See constants in {@link CarDrivingStateEvent}.
+         * @param speedRange If set, restrictions will only apply when current speed is within
+         *                   the range. Only {@link CarDrivingStateEvent#DRIVING_STATE_MOVING}
+         *                   supports speed range. {@code null} implies the full speed range,
+         *                   i.e. zero to {@link SpeedRange#MAX_SPEED}.
+         * @param requiresOptimization Whether distraction optimization (DO) is required for this
+         *                             driving state.
+         * @param restrictions See constants in {@link CarUxRestrictions}.
+         */
+        public Builder setUxRestrictions(@CarDrivingState int drivingState,
+                SpeedRange speedRange, boolean requiresOptimization,
+                @CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
+            if (drivingState != CarDrivingStateEvent.DRIVING_STATE_MOVING) {
+                if (speedRange != null) {
+                    throw new IllegalArgumentException(
+                            "Non-moving driving state cannot specify speed range.");
+                }
+                if (mUxRestrictions.get(drivingState).size() > 0) {
+                    throw new IllegalArgumentException("Non-moving driving state cannot have "
+                            + "more than one set of restrictions.");
+                }
+            }
+
+            mUxRestrictions.get(drivingState).add(
+                    new RestrictionsPerSpeedRange(requiresOptimization, restrictions, speedRange));
+            return this;
+        }
+
+        /**
+         * Sets max string length.
+         */
+        public Builder setMaxStringLength(int maxStringLength) {
+            mMaxStringLength = maxStringLength;
+            return this;
+        }
+
+        /**
+         * Sets max cumulative content items.
+         */
+        public Builder setMaxCumulativeContentItems(int maxCumulativeContentItems) {
+            mMaxCumulativeContentItems = maxCumulativeContentItems;
+            return this;
+        }
+
+        /**
+         * Sets max content depth.
+         */
+        public Builder setMaxContentDepth(int maxContentDepth) {
+            mMaxContentDepth = maxContentDepth;
+            return this;
+        }
+
+        /**
+         * @return CarUxRestrictionsConfiguration based on builder configuration.
+         */
+        public CarUxRestrictionsConfiguration build() {
+            // Create default restriction for unspecified driving state.
+            for (int drivingState : DRIVING_STATES) {
+                List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
+                if (restrictions.size() == 0) {
+                    Log.i(TAG, "Using default restrictions for driving state: "
+                            + getDrivingStateName(drivingState));
+                    restrictions.add(new RestrictionsPerSpeedRange(
+                            true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED));
+                }
+            }
+
+            // Configuration validation.
+            for (int drivingState : DRIVING_STATES) {
+                List<RestrictionsPerSpeedRange> restrictions = mUxRestrictions.get(drivingState);
+
+                if (drivingState == CarDrivingStateEvent.DRIVING_STATE_MOVING) {
+                    // Sort restrictions based on speed range.
+                    Collections.sort(restrictions,
+                            (r1, r2) -> r1.mSpeedRange.compareTo(r2.mSpeedRange));
+
+                    if (!isAllSpeedRangeCovered(restrictions)) {
+                        throw new IllegalStateException(
+                                "Moving state should cover full speed range.");
+                    }
+                } else {
+                    if (restrictions.size() != 1) {
+                        throw new IllegalStateException("Non-moving driving state should contain "
+                                + "one set of restriction rules.");
+                    }
+                }
+            }
+            return new CarUxRestrictionsConfiguration(this);
+        }
+
+        /**
+         * restrictions should be sorted based on speed range.
+         */
+        private boolean isAllSpeedRangeCovered(List<RestrictionsPerSpeedRange> restrictions) {
+            if (restrictions.size() == 1) {
+                if (restrictions.get(0).mSpeedRange == null) {
+                    // Single restriction with null speed range implies that
+                    // it applies to the entire driving state.
+                    return true;
+                }
+                return restrictions.get(0).mSpeedRange.mMinSpeed == 0
+                        && Float.compare(restrictions.get(0).mSpeedRange.mMaxSpeed,
+                        SpeedRange.MAX_SPEED) == 0;
+            }
+
+            if (restrictions.get(0).mSpeedRange.mMinSpeed != 0) {
+                Log.e(TAG, "Speed range min speed should start at 0.");
+                return false;
+            }
+            for (int i = 1; i < restrictions.size(); i++) {
+                RestrictionsPerSpeedRange prev = restrictions.get(i - 1);
+                RestrictionsPerSpeedRange curr = restrictions.get(i);
+                // If current min != prev.max, there's either an overlap or a gap in speed range.
+                if (Float.compare(curr.mSpeedRange.mMinSpeed, prev.mSpeedRange.mMaxSpeed) != 0) {
+                    Log.e(TAG, "Mis-configured speed range. Possibly speed range overlap or gap.");
+                    return false;
+                }
+            }
+            // The last speed range should have max speed.
+            float lastMaxSpeed = restrictions.get(restrictions.size() - 1).mSpeedRange.mMaxSpeed;
+            return lastMaxSpeed == SpeedRange.MAX_SPEED;
+        }
+
+        /**
+         * Speed range is defined by min and max speed. When there is no upper bound for max speed,
+         * set it to {@link SpeedRange#MAX_SPEED}.
+         */
+        public static final class SpeedRange implements Comparable<SpeedRange> {
+            public static final float MAX_SPEED = Float.POSITIVE_INFINITY;
+
+            private float mMinSpeed;
+            private float mMaxSpeed;
+
+            /**
+             * Defaults max speed to {@link SpeedRange#MAX_SPEED}.
+             */
+            public SpeedRange(@FloatRange(from = 0.0) float minSpeed) {
+                this(minSpeed, MAX_SPEED);
+            }
+
+            public SpeedRange(@FloatRange(from = 0.0) float minSpeed,
+                    @FloatRange(from = 0.0) float maxSpeed) {
+                if (minSpeed == MAX_SPEED) {
+                    throw new IllegalArgumentException("Min speed cannot be MAX_SPEED.");
+                }
+                if (maxSpeed < 0) {
+                    throw new IllegalArgumentException("Max speed cannot be negative.");
+                }
+                if (minSpeed > maxSpeed) {
+                    throw new IllegalArgumentException("Min speed " + minSpeed
+                            + " should not be greater than max speed " + maxSpeed);
+                }
+                mMinSpeed = minSpeed;
+                mMaxSpeed = maxSpeed;
+            }
+
+             /**
+             * Return if the given speed is in the range of [minSpeed, maxSpeed).
+             *
+             * @param speed Speed to check
+             * @return {@code true} if in range; {@code false} otherwise.
+             */
+            public boolean includes(float speed) {
+                if (speed < mMinSpeed) {
+                    return false;
+                }
+                if (mMaxSpeed == MAX_SPEED) {
+                    return true;
+                }
+                return speed < mMaxSpeed;
+            }
+
+            @Override
+            public int compareTo(SpeedRange other) {
+                // First compare min speed; then max speed.
+                int minSpeedComparison = Float.compare(this.mMinSpeed, other.mMinSpeed);
+                if (minSpeedComparison != 0) {
+                    return minSpeedComparison;
+                }
+
+                return Float.compare(this.mMaxSpeed, other.mMaxSpeed);
+            }
+
+            @Override
+            public boolean equals(Object obj) {
+                if (this == obj) {
+                    return true;
+                }
+                if (obj == null || !(obj instanceof SpeedRange)) {
+                    return false;
+                }
+                SpeedRange other = (SpeedRange) obj;
+
+                return this.compareTo(other) == 0;
+            }
+        }
+    }
+
+    /**
+     * Container for UX restrictions for a speed range.
+     * Speed range is valid only for the {@link CarDrivingStateEvent#DRIVING_STATE_MOVING}.
+     * @hide
+     */
+    public static final class RestrictionsPerSpeedRange implements Parcelable {
+        final boolean mReqOpt;
+        final int mRestrictions;
+        @Nullable
+        final Builder.SpeedRange mSpeedRange;
+
+        public RestrictionsPerSpeedRange(boolean reqOpt, int restrictions) {
+            this(reqOpt, restrictions, null);
+        }
+
+        public RestrictionsPerSpeedRange(boolean reqOpt, int restrictions,
+                @Nullable Builder.SpeedRange speedRange) {
+            if (!reqOpt && restrictions != CarUxRestrictions.UX_RESTRICTIONS_BASELINE) {
+                throw new IllegalArgumentException(
+                        "Driving optimization is not required but UX restrictions is required.");
+            }
+            mReqOpt = reqOpt;
+            mRestrictions = restrictions;
+            mSpeedRange = speedRange;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || !(obj instanceof RestrictionsPerSpeedRange)) {
+                return false;
+            }
+            RestrictionsPerSpeedRange other = (RestrictionsPerSpeedRange) obj;
+            return mReqOpt == other.mReqOpt
+                    && mRestrictions == other.mRestrictions
+                    && ((mSpeedRange == null && other.mSpeedRange == null) || mSpeedRange.equals(
+                    other.mSpeedRange));
+        }
+
+        // Parcelable methods/fields.
+
+        public static final Creator<RestrictionsPerSpeedRange> CREATOR =
+                new Creator<RestrictionsPerSpeedRange>() {
+                    @Override
+                    public RestrictionsPerSpeedRange createFromParcel(Parcel in) {
+                        return new RestrictionsPerSpeedRange(in);
+                    }
+
+                    @Override
+                    public RestrictionsPerSpeedRange[] newArray(int size) {
+                        return new RestrictionsPerSpeedRange[size];
+                    }
+                };
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        protected RestrictionsPerSpeedRange(Parcel in) {
+            mReqOpt = in.readBoolean();
+            mRestrictions = in.readInt();
+            // Whether speed range is specified.
+            Builder.SpeedRange speedRange = null;
+            if (in.readBoolean()) {
+                float minSpeed = in.readFloat();
+                float maxSpeed = in.readFloat();
+                speedRange = new Builder.SpeedRange(minSpeed, maxSpeed);
+            }
+            mSpeedRange = speedRange;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeBoolean(mReqOpt);
+            dest.writeInt(mRestrictions);
+            // Whether speed range is specified.
+            dest.writeBoolean(mSpeedRange != null);
+            if (mSpeedRange != null) {
+                dest.writeFloat(mSpeedRange.mMinSpeed);
+                dest.writeFloat(mSpeedRange.mMaxSpeed);
+            }
+        }
+    }
+}
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
index 57e7d60..b93b9e3 100644
--- a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
@@ -18,10 +18,10 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.car.Car;
 import android.car.CarManagerBase;
 import android.car.CarNotConnectedException;
-import android.car.drivingstate.ICarUxRestrictionsManager;
 import android.content.Context;
 import android.os.Handler;
 import android.os.IBinder;
@@ -117,6 +117,31 @@
     }
 
     /**
+     * Set a new {@link CarUxRestrictionsConfiguration} for next trip.
+     * <p>
+     * Saving a new configuration does not affect current configuration. The new configuration will
+     * only be used after UX Restrictions service restarts when the vehicle is parked.
+     * <p>
+     * Requires Permission:
+     * {@link android.car.Manifest.permission#CAR_UX_RESTRICTIONS_CONFIGURATION}.
+     *
+     * @param config UX restrictions configuration to be persisted.
+     * @return {@code true} if input config was successfully saved; {@code false} otherwise.
+     *
+     * @hide
+     */
+    @RequiresPermission(value = Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION)
+    public synchronized boolean saveUxRestrictionsConfigurationForNextBoot(
+            CarUxRestrictionsConfiguration config) throws CarNotConnectedException {
+        try {
+            return mUxRService.saveUxRestrictionsConfigurationForNextBoot(config);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not save new UX restrictions configuration", e);
+            throw new CarNotConnectedException(e);
+        }
+    }
+
+    /**
      * Unregister the registered {@link OnUxRestrictionsChangedListener}
      */
     public synchronized void unregisterListener()
diff --git a/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl b/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
index e5c69b8..270c74e 100644
--- a/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
+++ b/car-lib/src/android/car/drivingstate/ICarUxRestrictionsManager.aidl
@@ -17,6 +17,7 @@
 package android.car.drivingstate;
 
 import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
 import android.car.drivingstate.ICarUxRestrictionsChangeListener;
 
 /**
@@ -30,4 +31,5 @@
     void registerUxRestrictionsChangeListener(in ICarUxRestrictionsChangeListener listener) = 0;
     void unregisterUxRestrictionsChangeListener(in ICarUxRestrictionsChangeListener listener) = 1;
     CarUxRestrictions getCurrentUxRestrictions() = 2;
+    boolean saveUxRestrictionsConfigurationForNextBoot(in CarUxRestrictionsConfiguration config) = 3;
 }
diff --git a/car-lib/src/android/car/media/CarAudioManager.java b/car-lib/src/android/car/media/CarAudioManager.java
index 2b9e5af..4aa1ebf 100644
--- a/car-lib/src/android/car/media/CarAudioManager.java
+++ b/car-lib/src/android/car/media/CarAudioManager.java
@@ -16,70 +16,69 @@
 package android.car.media;
 
 import android.annotation.NonNull;
-import android.annotation.SystemApi;
 import android.car.CarLibLog;
 import android.car.CarManagerBase;
 import android.car.CarNotConnectedException;
-import android.content.ContentResolver;
 import android.content.Context;
-import android.database.ContentObserver;
 import android.media.AudioAttributes;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.provider.Settings;
 import android.util.Log;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
- * APIs for handling car specific audio stuff.
+ * APIs for handling audio in a car.
+ *
+ * In a car environment, we introduced the support to turn audio dynamic routing on /off by
+ * setting the "audioUseDynamicRouting" attribute in config.xml
+ *
+ * When audio dynamic routing is enabled:
+ * - Audio devices are grouped into zones
+ * - There is at least one primary zone, and extra secondary zones such as RSE
+ *   (Reat Seat Entertainment)
+ * - Within each zone, audio devices are grouped into volume groups for volume control
+ * - Audio is assigned to an audio device based on its AudioAttributes usage
+ *
+ * When audio dynamic routing is disabled:
+ * - There is exactly one audio zone, which is the primary zone
+ * - Each volume group represents a controllable STREAM_TYPE, same as AudioManager
  */
 public final class CarAudioManager implements CarManagerBase {
 
-    // The trailing slash forms a directory-liked hierarchy and
-    // allows listening for both GROUP/MEDIA and GROUP/NAVIGATION.
-    private static final String VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX = "android.car.VOLUME_GROUP/";
-
     /**
-     * @param groupId The volume group id
-     * @return Key to persist volume index for volume group in {@link Settings.Global}
+     * Zone id of the primary audio zone.
      */
-    public static String getVolumeSettingsKeyForGroup(int groupId) {
-        return VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX + groupId;
-    }
+    public static final int PRIMARY_AUDIO_ZONE = 0x0;
 
-    /**
-     * Key to persist master mute state in {@link Settings.Global}
-     *
-     * @hide
-     */
-    public static final String VOLUME_SETTINGS_KEY_MASTER_MUTE = "android.car.MASTER_MUTE";
-
-    private final ContentResolver mContentResolver;
     private final ICarAudio mService;
+    private final List<CarVolumeCallback> mCarVolumeCallbacks;
+
+    private final ICarVolumeCallback mCarVolumeCallbackImpl = new ICarVolumeCallback.Stub() {
+        @Override
+        public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
+            for (CarVolumeCallback callback : mCarVolumeCallbacks) {
+                callback.onGroupVolumeChanged(zoneId, groupId, flags);
+            }
+        }
+
+        @Override
+        public void onMasterMuteChanged(int zoneId, int flags) {
+            for (CarVolumeCallback callback : mCarVolumeCallbacks) {
+                callback.onMasterMuteChanged(zoneId, flags);
+            }
+        }
+    };
 
     /**
-     * Registers a {@link ContentObserver} to listen for volume group changes.
-     * Note that this observer is valid for bus based car audio stack only.
+     * Sets the volume index for a volume group in primary zone.
      *
-     * {@link ContentObserver#onChange(boolean)} will be called on every group volume change.
-     *
-     * @param observer The {@link ContentObserver} instance to register, non-null
+     * @see {@link #setGroupVolume(int, int, int, int)}
      */
-    @SystemApi
-    public void registerVolumeChangeObserver(@NonNull ContentObserver observer) {
-        mContentResolver.registerContentObserver(
-                Settings.Global.getUriFor(VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX),
-                true, observer);
-    }
-
-    /**
-     * Unregisters the {@link ContentObserver} which listens for volume group changes.
-     *
-     * @param observer The {@link ContentObserver} instance to unregister, non-null
-     */
-    @SystemApi
-    public void unregisterVolumeChangeObserver(@NonNull ContentObserver observer) {
-        mContentResolver.unregisterContentObserver(observer);
+    public void setGroupVolume(int groupId, int index, int flags) throws CarNotConnectedException {
+        setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, index, flags);
     }
 
     /**
@@ -87,16 +86,17 @@
      *
      * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
      *
+     * @param zoneId The zone id whose volume group is affected.
      * @param groupId The volume group id whose volume index should be set.
      * @param index The volume index to set. See
-     *            {@link #getGroupMaxVolume(int)} for the largest valid value.
+     *            {@link #getGroupMaxVolume(int, int)} for the largest valid value.
      * @param flags One or more flags (e.g., {@link android.media.AudioManager#FLAG_SHOW_UI},
      *              {@link android.media.AudioManager#FLAG_PLAY_SOUND})
      */
-    @SystemApi
-    public void setGroupVolume(int groupId, int index, int flags) throws CarNotConnectedException {
+    public void setGroupVolume(int zoneId, int groupId, int index, int flags)
+            throws CarNotConnectedException {
         try {
-            mService.setGroupVolume(groupId, index, flags);
+            mService.setGroupVolume(zoneId, groupId, index, flags);
         } catch (RemoteException e) {
             Log.e(CarLibLog.TAG_CAR, "setGroupVolume failed", e);
             throw new CarNotConnectedException(e);
@@ -104,17 +104,26 @@
     }
 
     /**
+     * Returns the maximum volume index for a volume group in primary zone.
+     *
+     * @see {@link #getGroupMaxVolume(int, int)}
+     */
+    public int getGroupMaxVolume(int groupId) throws CarNotConnectedException {
+        return getGroupMaxVolume(PRIMARY_AUDIO_ZONE, groupId);
+    }
+
+    /**
      * Returns the maximum volume index for a volume group.
      *
      * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
      *
+     * @param zoneId The zone id whose volume group is queried.
      * @param groupId The volume group id whose maximum volume index is returned.
      * @return The maximum valid volume index for the given group.
      */
-    @SystemApi
-    public int getGroupMaxVolume(int groupId) throws CarNotConnectedException {
+    public int getGroupMaxVolume(int zoneId, int groupId) throws CarNotConnectedException {
         try {
-            return mService.getGroupMaxVolume(groupId);
+            return mService.getGroupMaxVolume(zoneId, groupId);
         } catch (RemoteException e) {
             Log.e(CarLibLog.TAG_CAR, "getGroupMaxVolume failed", e);
             throw new CarNotConnectedException(e);
@@ -122,17 +131,26 @@
     }
 
     /**
+     * Returns the minimum volume index for a volume group in primary zone.
+     *
+     * @see {@link #getGroupMinVolume(int, int)}
+     */
+    public int getGroupMinVolume(int groupId) throws CarNotConnectedException {
+        return getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId);
+    }
+
+    /**
      * Returns the minimum volume index for a volume group.
      *
      * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
      *
+     * @param zoneId The zone id whose volume group is queried.
      * @param groupId The volume group id whose minimum volume index is returned.
      * @return The minimum valid volume index for the given group, non-negative
      */
-    @SystemApi
-    public int getGroupMinVolume(int groupId) throws CarNotConnectedException {
+    public int getGroupMinVolume(int zoneId, int groupId) throws CarNotConnectedException {
         try {
-            return mService.getGroupMinVolume(groupId);
+            return mService.getGroupMinVolume(zoneId, groupId);
         } catch (RemoteException e) {
             Log.e(CarLibLog.TAG_CAR, "getGroupMinVolume failed", e);
             throw new CarNotConnectedException(e);
@@ -140,20 +158,29 @@
     }
 
     /**
+     * Returns the current volume index for a volume group in primary zone.
+     *
+     * @see {@link #getGroupVolume(int, int)}
+     */
+    public int getGroupVolume(int groupId) throws CarNotConnectedException {
+        return getGroupVolume(PRIMARY_AUDIO_ZONE, groupId);
+    }
+
+    /**
      * Returns the current volume index for a volume group.
      *
      * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
      *
+     * @param zoneId The zone id whose volume groups is queried.
      * @param groupId The volume group id whose volume index is returned.
      * @return The current volume index for the given group.
      *
-     * @see #getGroupMaxVolume(int)
-     * @see #setGroupVolume(int, int, int)
+     * @see #getGroupMaxVolume(int, int)
+     * @see #setGroupVolume(int, int, int, int)
      */
-    @SystemApi
-    public int getGroupVolume(int groupId) throws CarNotConnectedException {
+    public int getGroupVolume(int zoneId, int groupId) throws CarNotConnectedException {
         try {
-            return mService.getGroupVolume(groupId);
+            return mService.getGroupVolume(zoneId, groupId);
         } catch (RemoteException e) {
             Log.e(CarLibLog.TAG_CAR, "getGroupVolume failed", e);
             throw new CarNotConnectedException(e);
@@ -170,7 +197,6 @@
      *
      * @see #setBalanceTowardRight(float)
      */
-    @SystemApi
     public void setFadeTowardFront(float value) throws CarNotConnectedException {
         try {
             mService.setFadeTowardFront(value);
@@ -190,7 +216,6 @@
      *
      * @see #setFadeTowardFront(float)
      */
-    @SystemApi
     public void setBalanceTowardRight(float value) throws CarNotConnectedException {
         try {
             mService.setBalanceTowardRight(value);
@@ -213,7 +238,6 @@
      * @see #createAudioPatch(String, int, int)
      * @see #releaseAudioPatch(CarAudioPatchHandle)
      */
-    @SystemApi
     public @NonNull String[] getExternalSources() throws CarNotConnectedException {
         try {
             return mService.getExternalSources();
@@ -243,7 +267,6 @@
      * @see #getExternalSources()
      * @see #releaseAudioPatch(CarAudioPatchHandle)
      */
-    @SystemApi
     public CarAudioPatchHandle createAudioPatch(String sourceAddress,
             @AudioAttributes.AttributeUsage int usage, int gainInMillibels)
             throws CarNotConnectedException {
@@ -266,7 +289,6 @@
      * @see #getExternalSources()
      * @see #createAudioPatch(String, int, int)
      */
-    @SystemApi
     public void releaseAudioPatch(CarAudioPatchHandle patch) throws CarNotConnectedException {
         try {
             mService.releaseAudioPatch(patch);
@@ -277,16 +299,25 @@
     }
 
     /**
+     * Gets the count of available volume groups in primary zone.
+     *
+     * @see {@link #getVolumeGroupCount(int)}
+     */
+    public int getVolumeGroupCount() throws CarNotConnectedException {
+        return getVolumeGroupCount(PRIMARY_AUDIO_ZONE);
+    }
+
+    /**
      * Gets the count of available volume groups in the system.
      *
      * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
      *
+     * @param zoneId The zone id whois count of volume groups is queried.
      * @return Count of volume groups
      */
-    @SystemApi
-    public int getVolumeGroupCount() throws CarNotConnectedException {
+    public int getVolumeGroupCount(int zoneId) throws CarNotConnectedException {
         try {
-            return mService.getVolumeGroupCount();
+            return mService.getVolumeGroupCount(zoneId);
         } catch (RemoteException e) {
             Log.e(CarLibLog.TAG_CAR, "getVolumeGroupCount failed", e);
             throw new CarNotConnectedException(e);
@@ -294,18 +325,28 @@
     }
 
     /**
+     * Gets the volume group id for a given {@link AudioAttributes} usage in primary zone.
+     *
+     * @see {@link #getVolumeGroupIdForUsage(int, int)}
+     */
+    public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage)
+            throws CarNotConnectedException {
+        return getVolumeGroupIdForUsage(PRIMARY_AUDIO_ZONE, usage);
+    }
+
+    /**
      * Gets the volume group id for a given {@link AudioAttributes} usage.
      *
      * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
      *
+     * @param zoneId The zone id whose volume group is queried.
      * @param usage The {@link AudioAttributes} usage to get a volume group from.
      * @return The volume group id where the usage belongs to
      */
-    @SystemApi
-    public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage)
+    public int getVolumeGroupIdForUsage(int zoneId, @AudioAttributes.AttributeUsage int usage)
             throws CarNotConnectedException {
         try {
-            return mService.getVolumeGroupIdForUsage(usage);
+            return mService.getVolumeGroupIdForUsage(zoneId, usage);
         } catch (RemoteException e) {
             Log.e(CarLibLog.TAG_CAR, "getVolumeGroupIdForUsage failed", e);
             throw new CarNotConnectedException(e);
@@ -313,71 +354,98 @@
     }
 
     /**
-     * Gets array of {@link AudioAttributes} usages for a given volume group id.
+     * Gets array of {@link AudioAttributes} usages for a volume group in primary zone.
+     *
+     * @see {@link #getUsagesForVolumeGroupId(int, int)}
+     */
+    public @NonNull int[] getUsagesForVolumeGroupId(int groupId) throws CarNotConnectedException {
+        return getUsagesForVolumeGroupId(PRIMARY_AUDIO_ZONE, groupId);
+    }
+
+    /**
+     * Gets array of {@link AudioAttributes} usages for a volume group in a zone.
      *
      * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
      *
+     * @param zoneId The zone id whose volume group is queried.
      * @param groupId The volume group id whose associated audio usages is returned.
      * @return Array of {@link AudioAttributes} usages for a given volume group id
      */
-    @SystemApi
-    public @NonNull int[] getUsagesForVolumeGroupId(int groupId) throws CarNotConnectedException {
+    public @NonNull int[] getUsagesForVolumeGroupId(int zoneId, int groupId)
+            throws CarNotConnectedException {
         try {
-            return mService.getUsagesForVolumeGroupId(groupId);
+            return mService.getUsagesForVolumeGroupId(zoneId, groupId);
         } catch (RemoteException e) {
             Log.e(CarLibLog.TAG_CAR, "getUsagesForVolumeGroupId failed", e);
             throw new CarNotConnectedException(e);
         }
     }
 
-    /**
-     * Register {@link ICarVolumeCallback} to receive the volume key events
-     *
-     * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
-     *
-     * @param binder {@link IBinder} instance of {@link ICarVolumeCallback} to receive
-     *                              volume key event callbacks
-     * @throws CarNotConnectedException
-     */
-    @SystemApi
-    public void registerVolumeCallback(@NonNull IBinder binder)
-            throws CarNotConnectedException {
-        try {
-            mService.registerVolumeCallback(binder);
-        } catch (RemoteException e) {
-            Log.e(CarLibLog.TAG_CAR, "registerVolumeCallback failed", e);
-            throw new CarNotConnectedException(e);
-        }
-    }
-
-    /**
-     * Unregister {@link ICarVolumeCallback} from receiving volume key events
-     *
-     * Requires {@link android.car.Car#PERMISSION_CAR_CONTROL_AUDIO_VOLUME} permission.
-     *
-     * @param binder {@link IBinder} instance of {@link ICarVolumeCallback} to stop receiving
-     *                              volume key event callbacks
-     * @throws CarNotConnectedException
-     */
-    @SystemApi
-    public void unregisterVolumeCallback(@NonNull IBinder binder)
-            throws CarNotConnectedException {
-        try {
-            mService.unregisterVolumeCallback(binder);
-        } catch (RemoteException e) {
-            Log.e(CarLibLog.TAG_CAR, "unregisterVolumeCallback failed", e);
-            throw new CarNotConnectedException(e);
-        }
-    }
-
     /** @hide */
     @Override
     public void onCarDisconnected() {
+        if (mService != null) {
+            try {
+                mService.unregisterVolumeCallback(mCarVolumeCallbackImpl.asBinder());
+            } catch (RemoteException e) {
+                Log.e(CarLibLog.TAG_CAR, "unregisterVolumeCallback failed", e);
+            }
+        }
     }
 
     /** @hide */
     public CarAudioManager(IBinder service, Context context, Handler handler) {
-        mContentResolver = context.getContentResolver();
         mService = ICarAudio.Stub.asInterface(service);
+        mCarVolumeCallbacks = new ArrayList<>();
+
+        try {
+            mService.registerVolumeCallback(mCarVolumeCallbackImpl.asBinder());
+        } catch (RemoteException e) {
+            Log.e(CarLibLog.TAG_CAR, "registerVolumeCallback failed", e);
+        }
+    }
+
+    /**
+     * Registers a {@link CarVolumeCallback} to receive volume change callbacks
+     * @param callback {@link CarVolumeCallback} instance, can not be null
+     */
+    public void registerCarVolumeCallback(@NonNull CarVolumeCallback callback) {
+        mCarVolumeCallbacks.add(callback);
+    }
+
+    /**
+     * Unregisters a {@link CarVolumeCallback} from receiving volume change callbacks
+     * @param callback {@link CarVolumeCallback} instance previously registered, can not be null
+     */
+    public void unregisterCarVolumeCallback(@NonNull CarVolumeCallback callback) {
+        mCarVolumeCallbacks.remove(callback);
+    }
+
+    /**
+     * Callback interface to receive volume change events in a car.
+     * Extend this class and register it with {@link #registerCarVolumeCallback(CarVolumeCallback)}
+     * and unregister it via {@link #unregisterCarVolumeCallback(CarVolumeCallback)}
+     */
+    public abstract static class CarVolumeCallback {
+        /**
+         * This is called whenever a group volume is changed.
+         * The changed-to volume index is not included, the caller is encouraged to
+         * get the current group volume index via CarAudioManager.
+         *
+         * @param zoneId Id of the audio zone that volume change happens
+         * @param groupId Id of the volume group that volume is changed
+         * @param flags see {@link android.media.AudioManager} for flag definitions
+         */
+        public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {}
+
+        /**
+         * This is called whenever the master mute state is changed.
+         * The changed-to master mute state is not included, the caller is encouraged to
+         * get the current master mute state via AudioManager.
+         *
+         * @param zoneId Id of the audio zone that master mute state change happens
+         * @param flags see {@link android.media.AudioManager} for flag definitions
+         */
+        public void onMasterMuteChanged(int zoneId, int flags) {}
     }
 }
diff --git a/car-lib/src/android/car/media/ICarAudio.aidl b/car-lib/src/android/car/media/ICarAudio.aidl
index 353c168..c343c42 100644
--- a/car-lib/src/android/car/media/ICarAudio.aidl
+++ b/car-lib/src/android/car/media/ICarAudio.aidl
@@ -25,10 +25,10 @@
  * @hide
  */
 interface ICarAudio {
-    void setGroupVolume(int groupId, int index, int flags);
-    int getGroupMaxVolume(int groupId);
-    int getGroupMinVolume(int groupId);
-    int getGroupVolume(int groupId);
+    void setGroupVolume(int zoneId, int groupId, int index, int flags);
+    int getGroupMaxVolume(int zoneId, int groupId);
+    int getGroupMinVolume(int zoneId, int groupId);
+    int getGroupVolume(int zoneId, int groupId);
 
     void setFadeTowardFront(float value);
     void setBalanceTowardRight(float value);
@@ -37,9 +37,9 @@
     CarAudioPatchHandle createAudioPatch(in String sourceAddress, int usage, int gainInMillibels);
     void releaseAudioPatch(in CarAudioPatchHandle patch);
 
-    int getVolumeGroupCount();
-    int getVolumeGroupIdForUsage(int usage);
-    int[] getUsagesForVolumeGroupId(int groupId);
+    int getVolumeGroupCount(int zoneId);
+    int getVolumeGroupIdForUsage(int zoneId, int usage);
+    int[] getUsagesForVolumeGroupId(int zoneId, int groupId);
 
     /**
      * IBinder is ICarVolumeCallback but passed as IBinder due to aidl hidden.
diff --git a/car-lib/src/android/car/media/ICarVolumeCallback.aidl b/car-lib/src/android/car/media/ICarVolumeCallback.aidl
index 8540680..9672983 100644
--- a/car-lib/src/android/car/media/ICarVolumeCallback.aidl
+++ b/car-lib/src/android/car/media/ICarVolumeCallback.aidl
@@ -27,12 +27,12 @@
      * The changed-to volume index is not included, the caller is encouraged to
      * get the current group volume index via CarAudioManager.
      */
-    void onGroupVolumeChanged(int groupId, int flags);
+    void onGroupVolumeChanged(int zoneId, int groupId, int flags);
 
     /**
      * This is called whenever the master mute state is changed.
      * The changed-to master mute state is not included, the caller is encouraged to
      * get the current master mute state via AudioManager.
      */
-    void onMasterMuteChanged(int flags);
+    void onMasterMuteChanged(int zoneId, int flags);
 }
diff --git a/car-lib/src/android/car/vms/VmsSubscriberManager.java b/car-lib/src/android/car/vms/VmsSubscriberManager.java
index fe9e8c3..1416b41 100644
--- a/car-lib/src/android/car/vms/VmsSubscriberManager.java
+++ b/car-lib/src/android/car/vms/VmsSubscriberManager.java
@@ -155,11 +155,11 @@
         } catch (RemoteException e) {
             Log.e(TAG, "Could not connect: ", e);
             throw new CarNotConnectedException(e);
-        }
-
-        synchronized (mClientCallbackLock) {
-            mClientCallback = null;
-            mExecutor = null;
+        } finally {
+            synchronized (mClientCallbackLock) {
+                mClientCallback = null;
+                mExecutor = null;
+            }
         }
     }
 
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbHostController.java b/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
index 5c61a98..e4b6df3 100644
--- a/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
+++ b/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
@@ -25,7 +25,9 @@
 import android.os.Looper;
 import android.os.Message;
 import android.util.Log;
+
 import com.android.internal.annotations.GuardedBy;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -90,7 +92,6 @@
         filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
         filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
         context.registerReceiver(mUsbBroadcastReceiver, filter);
-
     }
 
     private synchronized void setActiveDeviceIfMatch(UsbDevice device) {
@@ -129,11 +130,11 @@
         return activeDevice != null && UsbUtil.isDevicesMatching(activeDevice, device);
     }
 
-    private String generateTitle() {
-        String manufacturer = mActiveDevice.getManufacturerName();
-        String product = mActiveDevice.getProductName();
+    private static String generateTitle(Context context, UsbDevice usbDevice) {
+        String manufacturer = usbDevice.getManufacturerName();
+        String product = usbDevice.getProductName();
         if (manufacturer == null && product == null) {
-            return mContext.getString(R.string.usb_unknown_device);
+            return context.getString(R.string.usb_unknown_device);
         }
         if (manufacturer != null && product != null) {
             return manufacturer + " " + product;
@@ -158,14 +159,14 @@
 
         UsbDeviceSettings settings = mUsbSettingsStorage.getSettings(device);
         if (settings != null && mUsbResolver.dispatch(
-                    mActiveDevice, settings.getHandler(), settings.getAoap())) {
+                    device, settings.getHandler(), settings.getAoap())) {
             if (LOCAL_LOGV) {
                 Log.v(TAG, "Usb Device: " + device + " was sent to component: "
                         + settings.getHandler());
             }
             return;
         }
-        mCallback.titleChanged(generateTitle());
+        mCallback.titleChanged(generateTitle(mContext, device));
         mUsbResolver.resolve(device);
     }
 
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index 7c08389..3e41d70 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -114,13 +114,6 @@
 PRODUCT_COPY_FILES += \
     packages/services/Car/car_product/bootanimations/bootanimation-832.zip:system/media/bootanimation.zip
 
-PRODUCT_PROPERTY_OVERRIDES += \
-    fmas.spkr_6ch=35,20,110 \
-    fmas.spkr_2ch=35,25 \
-    fmas.spkr_angles=10 \
-    fmas.spkr_sgain=0 \
-    media.aac_51_output_enabled=true
-
 PRODUCT_LOCALES := en_US af_ZA am_ET ar_EG bg_BG bn_BD ca_ES cs_CZ da_DK de_DE el_GR en_AU en_GB en_IN es_ES es_US et_EE eu_ES fa_IR fi_FI fr_CA fr_FR gl_ES hi_IN hr_HR hu_HU hy_AM in_ID is_IS it_IT iw_IL ja_JP ka_GE km_KH ko_KR ky_KG lo_LA lt_LT lv_LV km_MH kn_IN mn_MN ml_IN mk_MK mr_IN ms_MY my_MM ne_NP nb_NO nl_NL pl_PL pt_BR pt_PT ro_RO ru_RU si_LK sk_SK sl_SI sr_RS sv_SE sw_TZ ta_IN te_IN th_TH tl_PH tr_TR uk_UA vi_VN zh_CN zh_HK zh_TW zu_ZA en_XA ar_XB
 
 # should add to BOOT_JARS only once
diff --git a/car_product/build/car_base.mk b/car_product/build/car_base.mk
index 0e5ebab..385a2ea 100644
--- a/car_product/build/car_base.mk
+++ b/car_product/build/car_base.mk
@@ -42,6 +42,7 @@
     MmsService \
     ExternalStorageProvider \
     atrace \
+    cameraserver \
     libandroidfw \
     libaudioutils \
     libmdnssd \
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/config.xml b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
index 0ab814f..3ab6085 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
@@ -65,9 +65,6 @@
     <!-- The action buttons should always take the default color. -->
     <bool name="config_tintNotificationActionButtons">false</bool>
 
-    <!-- Home screen(Launcher) app presence -->
-    <bool name="config_noHomeScreen">true</bool>
-
     <!-- Flag indicating that this device does not rotate and will always remain in its default
          orientation. Activities that desire to run in a non-compatible orientation will find that
          they are not able to. -->
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml b/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
index f343e93..cf09310 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/dimens.xml
@@ -17,12 +17,12 @@
 */
 -->
 <resources>
-    <dimen name="status_bar_height">96dp</dimen>
-    <dimen name="status_bar_height_landscape">96dp</dimen>
-    <dimen name="status_bar_height_portrait">96dp</dimen>
-    <dimen name="car_qs_header_system_icons_area_height">96dp</dimen>
-    <dimen name="navigation_bar_height">128dp</dimen>
-    <dimen name="navigation_bar_height_landscape">128dp</dimen>
+    <dimen name="status_bar_height">76dp</dimen>
+    <dimen name="status_bar_height_landscape">76dp</dimen>
+    <dimen name="status_bar_height_portrait">76dp</dimen>
+    <dimen name="car_qs_header_system_icons_area_height">76dp</dimen>
+    <dimen name="navigation_bar_height">112dp</dimen>
+    <dimen name="navigation_bar_height_landscape">112dp</dimen>
     <dimen name="status_bar_icon_size">36dp</dimen>
 
     <!-- The height of the header of a notification. -->
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/ic_backspace.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/ic_backspace.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/ic_backspace.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/ic_backspace.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/ic_done.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/ic_done.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/ic_done.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/ic_done.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/keyguard_button_background.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/keyguard_button_background.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/drawable/keyguard_button_background.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/drawable/keyguard_button_background.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout-land/keyguard_pattern_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout-land/keyguard_pin_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout-land/keyguard_pin_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout-land/keyguard_pin_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout-land/keyguard_pin_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_bouncer.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_bouncer.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_bouncer.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_bouncer.xml
diff --git a/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_message_area.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_message_area.xml
new file mode 100644
index 0000000..c230414
--- /dev/null
+++ b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_message_area.xml
@@ -0,0 +1,31 @@
+<?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.
+*/
+-->
+
+<com.android.keyguard.KeyguardMessageArea
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    style="@style/Keyguard.TextView"
+    android:id="@+id/keyguard_message_area"
+    android:singleLine="true"
+    android:ellipsize="marquee"
+    android:focusable="true"
+    android:layout_marginBottom="@dimen/car_padding_4"
+    android:textSize="@dimen/car_body2_size" />
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_num_pad_key.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_password_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_pattern_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_pattern_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_pattern_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_pin_view.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/keyguard_pin_view.xml
diff --git a/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/num_pad_keys.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/num_pad_keys.xml
new file mode 100644
index 0000000..ca0595d
--- /dev/null
+++ b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/layout/num_pad_keys.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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
+  -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:app="http://schemas.android.com/apk/res-auto">
+    <!-- Row 1 -->
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key1"
+        style="@style/NumPadKeyButton"
+        app:digit="@string/one" />
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key2"
+        style="@style/NumPadKeyButton.MiddleColumn"
+        app:digit="@string/two" />
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key3"
+        style="@style/NumPadKeyButton"
+        app:digit="@string/three" />
+
+    <!-- Row 2 -->
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key4"
+        style="@style/NumPadKeyButton"
+        app:digit="@string/four" />
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key5"
+        style="@style/NumPadKeyButton.MiddleColumn"
+        app:digit="@string/five" />
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key6"
+        style="@style/NumPadKeyButton"
+        app:digit="@string/six" />
+
+    <!-- Row 3 -->
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key7"
+        style="@style/NumPadKeyButton"
+        app:digit="@string/seven" />
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key8"
+        style="@style/NumPadKeyButton.MiddleColumn"
+        app:digit="@string/eight" />
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key9"
+        style="@style/NumPadKeyButton"
+        app:digit="@string/nine" />
+
+    <!-- Row 4 -->
+    <ImageButton
+        android:id="@+id/delete_button"
+        style="@style/NumPadKeyButton.LastRow"
+        android:gravity="center_vertical"
+        android:src="@drawable/ic_backspace"
+        android:clickable="true"
+        android:tint="@android:color/white"
+        android:background="@drawable/ripple_drawable"
+        android:contentDescription="@string/keyboardview_keycode_delete" />
+    <com.android.keyguard.NumPadKey
+        android:id="@+id/key0"
+        style="@style/NumPadKeyButton.LastRow.MiddleColumn"
+        app:digit="@string/zero" />
+    <ImageButton
+        android:id="@+id/key_enter"
+        style="@style/NumPadKeyButton.LastRow"
+        android:src="@drawable/ic_done"
+        android:tint="@android:color/white"
+        android:background="@drawable/ripple_drawable"
+        android:contentDescription="@string/keyboardview_keycode_enter" />
+</merge>
+
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values-h1000dp/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-h1000dp/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values-h1000dp/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-h1000dp/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-land/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-land/dimens.xml
new file mode 100644
index 0000000..805a134
--- /dev/null
+++ b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values-land/dimens.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<resources>
+    <dimen name="num_pad_key_margin_horizontal">@dimen/car_padding_5</dimen>
+    <dimen name="num_pad_key_margin_bottom">@dimen/car_padding_4</dimen>
+</resources>
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/colors.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/colors.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/colors.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/colors.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/integers.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/integers.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/integers.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/integers.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/strings.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/strings.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/strings.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/strings.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/styles.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/styles.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res-keyguard/values/styles.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res-keyguard/values/styles.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values-h600dp/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-h600dp/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values-h600dp/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-h600dp/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values-sw600dp/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-sw600dp/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values-sw600dp/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-sw600dp/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values-w1024dp/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-w1024dp/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values-w1024dp/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-w1024dp/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values-w550dp-land/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-w550dp-land/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values-w550dp-land/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values-w550dp-land/dimens.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values/config.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values/config.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
diff --git a/car_product/overlay/frameworks/base/packages/SystemUI/res/values/dimens.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/dimens.xml
similarity index 100%
rename from car_product/overlay/frameworks/base/packages/SystemUI/res/values/dimens.xml
rename to car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/dimens.xml
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 4a16579..1f221ac 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -23,7 +23,7 @@
     <original-package android:name="com.android.car" />
      <permission-group
         android:name="android.car.permission-group.CAR_MONITORING"
-        android:icon="@drawable/car_ic_mode"
+        android:icon="@drawable/perm_group_car"
         android:description="@string/car_permission_desc"
         android:label="@string/car_permission_label" />
     <permission
@@ -188,29 +188,37 @@
         android:description="@string/car_permission_desc_audio_settings" />
 
     <permission
-            android:name="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"
-            android:protectionLevel="signature"
-            android:label="@string/car_permission_label_bind_instrument_cluster_rendering"
-            android:description="@string/car_permission_desc_bind_instrument_cluster_rendering"/>
+        android:name="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"
+        android:protectionLevel="signature"
+        android:label="@string/car_permission_label_bind_instrument_cluster_rendering"
+        android:description="@string/car_permission_desc_bind_instrument_cluster_rendering"/>
 
     <permission
-            android:name="android.car.permission.BIND_CAR_INPUT_SERVICE"
-            android:protectionLevel="signature"
-            android:label="@string/car_permission_label_bind_input_service"
-            android:description="@string/car_permission_desc_bind_input_service"/>
+        android:name="android.car.permission.BIND_CAR_INPUT_SERVICE"
+        android:protectionLevel="signature"
+        android:label="@string/car_permission_label_bind_input_service"
+        android:description="@string/car_permission_desc_bind_input_service"/>
 
     <permission
-            android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"
-            android:protectionLevel="system|signature"
-            android:label="@string/car_permission_car_display_in_cluster"
-            android:description="@string/car_permission_desc_car_display_in_cluster" />
+        android:name="android.car.permission.CAR_DISPLAY_IN_CLUSTER"
+        android:protectionLevel="system|signature"
+        android:label="@string/car_permission_car_display_in_cluster"
+        android:description="@string/car_permission_desc_car_display_in_cluster" />
 
-    <permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"
-                android:protectionLevel="system|signature"
-                android:label="@string/car_permission_car_cluster_control"
-                android:description="@string/car_permission_desc_car_cluster_control" />
+    <permission
+        android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"
+        android:protectionLevel="system|signature"
+        android:label="@string/car_permission_car_cluster_control"
+        android:description="@string/car_permission_desc_car_cluster_control" />
 
-    <permission android:name="android.car.permission.STORAGE_MONITORING"
+    <permission
+        android:name="android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION"
+        android:protectionLevel="system|signature"
+        android:label="@string/car_permission_label_car_ux_restrictions_configuration"
+        android:description="@string/car_permission_desc_car_ux_restrictions_configuration" />
+
+    <permission
+        android:name="android.car.permission.STORAGE_MONITORING"
         android:protectionLevel="system|signature"
         android:label="@string/car_permission_label_storage_monitoring"
         android:description="@string/car_permission_desc_storage_monitoring" />
diff --git a/service/res/drawable-hdpi/car_ic_mode.png b/service/res/drawable-hdpi/car_ic_mode.png
deleted file mode 100644
index a8f719f..0000000
--- a/service/res/drawable-hdpi/car_ic_mode.png
+++ /dev/null
Binary files differ
diff --git a/service/res/drawable-mdpi/car_ic_mode.png b/service/res/drawable-mdpi/car_ic_mode.png
deleted file mode 100644
index 38a9747..0000000
--- a/service/res/drawable-mdpi/car_ic_mode.png
+++ /dev/null
Binary files differ
diff --git a/service/res/drawable-xhdpi/car_ic_mode.png b/service/res/drawable-xhdpi/car_ic_mode.png
deleted file mode 100644
index 58a1aca..0000000
--- a/service/res/drawable-xhdpi/car_ic_mode.png
+++ /dev/null
Binary files differ
diff --git a/service/res/drawable/perm_group_car.xml b/service/res/drawable/perm_group_car.xml
new file mode 100644
index 0000000..79e3f03
--- /dev/null
+++ b/service/res/drawable/perm_group_car.xml
@@ -0,0 +1,24 @@
+<!--
+    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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="192.0"
+        android:viewportWidth="192.0">
+
+    <path android:fillColor="#FFFFFFFF"
+          android:pathData="M36.9,180.4l5.6,5.6L96,164l53.4,22l5.6,-5.6L96,73L36.9,180.4zM184,145.8L105.7,11.6C103.7,8.1 100,6 96,6c-4,0 -7.7,2.1 -9.8,5.6L7.5,146.5c-2,3.5 -2.1,7.3 0,10.8c2,3.5 5.8,5.7 9.8,5.7H38l54.6,-99h6.8l54.6,99h20.3c6.2,0 11.7,-4.6 11.7,-10.8C186,149.8 185.3,147.6 184,145.8z"/>
+</vector>
diff --git a/service/res/values/attrs.xml b/service/res/values/attrs.xml
index 89c4258..be20c0f 100644
--- a/service/res/values/attrs.xml
+++ b/service/res/values/attrs.xml
@@ -18,11 +18,11 @@
 -->
 
 <resources>
-    <!-- Defines the attributes and values used in res/xml/car_volume_group.xml -->
-    <declare-styleable name="volumeGroups" />
-
+    <!-- Defines the attributes and values used in res/xml/car_volume_groups.xml -->
+    <declare-styleable name="volumeGroups">
+        <attr name="isDeprecated" format="boolean"/>
+    </declare-styleable>
     <declare-styleable name="volumeGroups_group"/>
-
     <declare-styleable name="volumeGroups_context">
         <!-- Align with hardware/interfaces/automotive/audiocontrol/1.0/types.hal:ContextNumber -->
         <attr name="context">
@@ -37,6 +37,18 @@
         </attr>
     </declare-styleable>
 
+    <!--
+      Defines the attributes and values used in car_audio_configuration.xml
+      This is a superset of car_volume_groups.xml
+    -->
+    <declare-styleable name="carAudioConfiguration">
+        <attr name="version" format="integer"/>
+        <attr name="isPrimary" format="boolean"/>
+        <attr name="name" format="string"/>
+        <attr name="address" format="string"/>
+        <attr name="display" format="string"/>
+    </declare-styleable>
+
     <!-- Defines the UX restrictions to be imposed for different driving states of a vehicle -->
     <declare-styleable name="UxRestrictions"/>
     <!-- 1. UX restriction Mapping from a driving state of the vehicle-->
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 1d72dca..3b05e29 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -24,6 +24,14 @@
           dynamic audio routing is disabled and audio works in legacy mode. It may be useful
           during initial development where audio hal does not support bus based addressing yet. -->
     <bool name="audioUseDynamicRouting">false</bool>
+
+    <!--  Configuration to use the unified audio configuration.
+          This flag has no effect if audioUseDynamicRouting is set to false.
+          When audioUseDynamicRouting is enabled
+          - car_volume_groups.xml will be picked if this flag is false
+          - car_audio_configuration.xml will be used otherwise. -->
+    <bool name="audioUseUnifiedConfiguration">false</bool>
+
     <!--  Configuration to persist master mute state. If this is set to true,
           Android will restore the master mute state on boot. -->
     <bool name="audioPersistMasterMuteState">true</bool>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index 599e41f..2a7bcaa 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -90,6 +90,8 @@
     <string name="car_permission_desc_car_cluster_control">Launch apps in the instrument cluster</string>
     <string name="car_permission_label_bind_instrument_cluster_rendering">Instrument Cluster Rendering</string>
     <string name="car_permission_desc_bind_instrument_cluster_rendering">Receive instrument cluster data</string>
+    <string name="car_permission_label_car_ux_restrictions_configuration">UX Restrictions Configuration</string>
+    <string name="car_permission_desc_car_ux_restrictions_configuration">Configure UX Restrictions</string>
 
     <!-- Permission text: apps can handle input events [CHAR LIMIT=NONE] -->
     <string name="car_permission_label_bind_input_service">Car Input Service</string>
diff --git a/service/res/xml/car_audio_configuration.xml b/service/res/xml/car_audio_configuration.xml
new file mode 100644
index 0000000..e3fd8f6
--- /dev/null
+++ b/service/res/xml/car_audio_configuration.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!--
+  Defines the audio configuration in a car, including
+    - Audio zones
+    - Display to audio zone mappings
+    - Context to audio bus mappings
+    - Volume groups
+  in the car environment.
+-->
+<carAudioConfiguration
+        xmlns:car="http://schemas.android.com/apk/res-auto"
+        car:version="1">
+    <zones>
+        <zone car:name="primary zone" car:isPrimary="true">
+            <volumeGroups>
+                <group>
+                    <device car:address="bus0_media_out">
+                        <context car:context="music"/>
+                    </device>
+                    <device car:address="bus3_call_ring_out">
+                        <context car:context="call_ring"/>
+                    </device>
+                    <device car:address="bus6_notification_out">
+                        <context car:context="notification"/>
+                    </device>
+                    <device car:address="bus7_system_sound_out">
+                        <context car:context="system_sound"/>
+                    </device>
+                </group>
+                <group>
+                    <device car:address="bus1_navigation_out">
+                        <context car:context="navigation"/>
+                    </device>
+                    <device car:address="bus2_voice_command_out">
+                        <context car:context="voice_command"/>
+                    </device>
+                </group>
+                <group>
+                    <device car:address="bus4_call_out">
+                        <context car:context="call"/>
+                    </device>
+                </group>
+                <group>
+                    <device car:address="bus5_alarm_out">
+                        <context car:context="alarm"/>
+                    </device>
+                </group>
+            </volumeGroups>
+            <displays>
+                <display car:display="primary_display"/>
+            </displays>
+        </zone>
+        <zone car:name="rear seat zone">
+            <volumeGroups>
+                <group>
+                    <device car:address="bus100_rear_seat">
+                        <context car:context="music"/>
+                        <context car:context="navigation"/>
+                        <context car:context="voice_command"/>
+                        <context car:context="call_ring"/>
+                        <context car:context="call"/>
+                        <context car:context="alarm"/>
+                        <context car:context="notification"/>
+                        <context car:context="system_sound"/>
+                    </device>
+                </group>
+            </volumeGroups>
+            <displays>
+                <display car:display="rear_seat_display"/>
+            </displays>
+        </zone>
+    </zones>
+</carAudioConfiguration>
diff --git a/service/res/xml/car_volume_groups.xml b/service/res/xml/car_volume_groups.xml
index 9bfc305..c900329 100644
--- a/service/res/xml/car_volume_groups.xml
+++ b/service/res/xml/car_volume_groups.xml
@@ -15,6 +15,18 @@
 -->
 
 <!--
+  This configuration is replaced by car_audio_configuration.xml
+
+  Notes on backward compatibility
+  - A new audioUseUnifiedConfiguration flag is added, and the default value
+  is false
+  - If OEM does not explicitly set audioUseUnifiedConfiguration to be true,
+  car_volume_groups.xml will be used, CarAudioService also queries
+  IAudioControl HAL (getBusForContext)
+  - Otherwise, CarAudioService loads the new car_audio_configuration.xml
+
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
   Defines the all available volume groups for volume control in a car.
   One can overlay this configuration to customize the groups.
 
@@ -33,7 +45,8 @@
   Important note: when overlaying this configuration,
   make sure the resources are in the same package as CarAudioService.
 -->
-<volumeGroups xmlns:car="http://schemas.android.com/apk/res-auto">
+<volumeGroups xmlns:car="http://schemas.android.com/apk/res-auto"
+        car:isDeprecated="true">
     <group>
         <context car:context="music"/>
         <context car:context="call_ring"/>
diff --git a/service/src/com/android/car/CarUxRestrictionsConfigurationXmlParser.java b/service/src/com/android/car/CarUxRestrictionsConfigurationXmlParser.java
new file mode 100644
index 0000000..95bcb2c
--- /dev/null
+++ b/service/src/com/android/car/CarUxRestrictionsConfigurationXmlParser.java
@@ -0,0 +1,316 @@
+/*
+ * 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;
+
+import android.annotation.Nullable;
+import android.annotation.XmlRes;
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * @hide
+ */
+public final class CarUxRestrictionsConfigurationXmlParser {
+    private static final String TAG = "UxRConfigParser";
+    private static final int UX_RESTRICTIONS_UNKNOWN = -1;
+    private static final float INVALID_SPEED = -1f;
+    // XML tags to parse
+    private static final String ROOT_ELEMENT = "UxRestrictions";
+    private static final String RESTRICTION_MAPPING = "RestrictionMapping";
+    private static final String RESTRICTION_PARAMETERS = "RestrictionParameters";
+    private static final String DRIVING_STATE = "DrivingState";
+    private static final String RESTRICTIONS = "Restrictions";
+    private static final String STRING_RESTRICTIONS = "StringRestrictions";
+    private static final String CONTENT_RESTRICTIONS = "ContentRestrictions";
+
+    private final Context mContext;
+
+    private CarUxRestrictionsConfiguration.Builder mConfigBuilder;
+
+    private CarUxRestrictionsConfigurationXmlParser(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Loads the UX restrictions related information from the XML resource.
+     *
+     * @return parsed CarUxRestrictionsConfiguration; {@code null} if the XML is malformed.
+     */
+    @Nullable
+    public static CarUxRestrictionsConfiguration parse(Context context, @XmlRes int xmlResource)
+            throws IOException, XmlPullParserException {
+        return new CarUxRestrictionsConfigurationXmlParser(context).parse(xmlResource);
+    }
+
+    @Nullable
+    private CarUxRestrictionsConfiguration parse(@XmlRes int xmlResource)
+            throws IOException, XmlPullParserException {
+        mConfigBuilder = new CarUxRestrictionsConfiguration.Builder();
+
+        XmlResourceParser parser = mContext.getResources().getXml(xmlResource);
+        if (parser == null) {
+            Log.e(TAG, "Invalid Xml resource");
+            return null;
+        }
+
+        if (!traverseUntilStartTag(parser)) {
+            Log.e(TAG, "XML root element invalid: " + parser.getName());
+            return null;
+        }
+
+        if (!traverseUntilEndOfDocument(parser)) {
+            Log.e(TAG, "Could not parse XML to end");
+            return null;
+        }
+
+        return mConfigBuilder.build();
+    }
+
+    private boolean traverseUntilStartTag(XmlResourceParser parser)
+            throws IOException, XmlPullParserException {
+        int type;
+        // Traverse till we get to the first tag
+        while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+                && type != XmlResourceParser.START_TAG) {
+            // Do nothing.
+        }
+        return ROOT_ELEMENT.equals(parser.getName());
+    }
+
+    private boolean traverseUntilEndOfDocument(XmlResourceParser parser)
+            throws XmlPullParserException, IOException {
+        AttributeSet attrs = Xml.asAttributeSet(parser);
+        while (parser.getEventType() != XmlResourceParser.END_DOCUMENT) {
+            // Every time we hit a start tag, check for the type of the tag
+            // and load the corresponding information.
+            if (parser.next() == XmlResourceParser.START_TAG) {
+                switch (parser.getName()) {
+                    case RESTRICTION_MAPPING:
+                        if (!mapDrivingStateToRestrictions(parser, attrs)) {
+                            Log.e(TAG, "Could not map driving state to restriction.");
+                            return false;
+                        }
+                        break;
+                    case RESTRICTION_PARAMETERS:
+                        if (!parseRestrictionParameters(parser, attrs)) {
+                            // Failure to parse is automatically handled by falling back to
+                            // defaults. Just log the information here.
+                            if (Log.isLoggable(TAG, Log.INFO)) {
+                                Log.i(TAG, "Error reading restrictions parameters. "
+                                        + "Falling back to platform defaults.");
+                            }
+                        }
+                        break;
+                    default:
+                        Log.w(TAG, "Unknown class:" + parser.getName());
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Parses the information in the <restrictionMapping> tag to construct the mapping from
+     * driving state to UX restrictions.
+     */
+    private boolean mapDrivingStateToRestrictions(XmlResourceParser parser, AttributeSet attrs)
+            throws IOException, XmlPullParserException {
+        if (parser == null || attrs == null) {
+            Log.e(TAG, "Invalid arguments");
+            return false;
+        }
+        // The parser should be at the <RestrictionMapping> tag at this point.
+        if (!RESTRICTION_MAPPING.equals(parser.getName())) {
+            Log.e(TAG, "Parser not at RestrictionMapping element: " + parser.getName());
+            return false;
+        }
+        if (!traverseToTag(parser, DRIVING_STATE)) {
+            Log.e(TAG, "No <" + DRIVING_STATE + "> tag in XML");
+            return false;
+        }
+        // Handle all the <DrivingState> tags.
+        while (DRIVING_STATE.equals(parser.getName())) {
+            if (parser.getEventType() == XmlResourceParser.START_TAG) {
+                // 1. Get the driving state attributes: driving state and speed range
+                TypedArray a = mContext.getResources().obtainAttributes(attrs,
+                        R.styleable.UxRestrictions_DrivingState);
+                int drivingState = a
+                        .getInt(R.styleable.UxRestrictions_DrivingState_state,
+                                CarDrivingStateEvent.DRIVING_STATE_UNKNOWN);
+                float minSpeed = a
+                        .getFloat(
+                                R.styleable
+                                        .UxRestrictions_DrivingState_minSpeed,
+                                INVALID_SPEED);
+                float maxSpeed = a
+                        .getFloat(
+                                R.styleable
+                                        .UxRestrictions_DrivingState_maxSpeed,
+                                INVALID_SPEED);
+                a.recycle();
+
+                // 2. Traverse to the <Restrictions> tag
+                if (!traverseToTag(parser, RESTRICTIONS)) {
+                    Log.e(TAG, "No <" + RESTRICTIONS + "> tag in XML");
+                    return false;
+                }
+
+                // 3. Parse the restrictions for this driving state
+                Pair<Boolean, Integer> restrictions = parseRestrictions(parser, attrs);
+                if (Log.isLoggable(TAG, Log.DEBUG)) {
+                    Log.d(TAG, "Map " + drivingState + " : " + restrictions);
+                }
+
+                // Update the builder if the driving state and restrictions info are valid.
+                if (drivingState != CarDrivingStateEvent.DRIVING_STATE_UNKNOWN
+                        && restrictions != null) {
+                    addToRestrictions(drivingState, minSpeed, maxSpeed, restrictions.first,
+                            restrictions.second);
+                }
+            }
+            parser.next();
+        }
+        return true;
+    }
+
+    /**
+     * Parses the <restrictions> tag nested with the <drivingState>.  This provides the restrictions
+     * for the enclosing driving state.
+     */
+    @Nullable
+    private Pair<Boolean, Integer> parseRestrictions(XmlResourceParser parser, AttributeSet attrs)
+            throws IOException, XmlPullParserException {
+        int restrictions = UX_RESTRICTIONS_UNKNOWN;
+        boolean requiresOpt = true;
+        if (parser == null || attrs == null) {
+            Log.e(TAG, "Invalid Arguments");
+            return null;
+        }
+
+        while (RESTRICTIONS.equals(parser.getName())
+                && parser.getEventType() == XmlResourceParser.START_TAG) {
+            TypedArray a = mContext.getResources().obtainAttributes(attrs,
+                    R.styleable.UxRestrictions_Restrictions);
+            restrictions = a.getInt(
+                    R.styleable.UxRestrictions_Restrictions_uxr,
+                    CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED);
+            requiresOpt = a.getBoolean(
+                    R.styleable.UxRestrictions_Restrictions_requiresDistractionOptimization, true);
+            a.recycle();
+            parser.next();
+        }
+        return new Pair<>(requiresOpt, restrictions);
+    }
+
+    private void addToRestrictions(int drivingState, float minSpeed, float maxSpeed,
+            boolean requiresOpt, int restrictions) {
+        CarUxRestrictionsConfiguration.Builder.SpeedRange speedRange = null;
+        if (Float.compare(minSpeed, INVALID_SPEED) != 0) {
+            if (Float.compare(maxSpeed, INVALID_SPEED) == 0) {
+                // Setting min speed but not max implies MAX_SPEED.
+                maxSpeed = CarUxRestrictionsConfiguration.Builder.SpeedRange.MAX_SPEED;
+            }
+            speedRange = new CarUxRestrictionsConfiguration.Builder.SpeedRange(minSpeed, maxSpeed);
+        }
+        mConfigBuilder.setUxRestrictions(drivingState, speedRange, requiresOpt, restrictions);
+    }
+
+    private boolean traverseToTag(XmlResourceParser parser, String tag)
+            throws IOException, XmlPullParserException {
+        if (tag == null || parser == null) {
+            return false;
+        }
+        int type;
+        while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT) {
+            if (type == XmlResourceParser.START_TAG && parser.getName().equals(tag)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Parses the information in the <RestrictionParameters> tag to read the parameters for the
+     * applicable UX restrictions
+     */
+    private boolean parseRestrictionParameters(XmlResourceParser parser, AttributeSet attrs)
+            throws IOException, XmlPullParserException {
+        if (parser == null || attrs == null) {
+            Log.e(TAG, "Invalid arguments");
+            return false;
+        }
+        // The parser should be at the <RestrictionParameters> tag at this point.
+        if (!RESTRICTION_PARAMETERS.equals(parser.getName())) {
+            Log.e(TAG, "Parser not at RestrictionParameters element: " + parser.getName());
+            return false;
+        }
+        while (parser.getEventType() != XmlResourceParser.END_DOCUMENT) {
+            int type = parser.next();
+            // Break if we have parsed all <RestrictionParameters>
+            if (type == XmlResourceParser.END_TAG && RESTRICTION_PARAMETERS.equals(
+                    parser.getName())) {
+                return true;
+            }
+            if (type == XmlResourceParser.START_TAG) {
+                TypedArray a = null;
+                switch (parser.getName()) {
+                    case STRING_RESTRICTIONS:
+                        a = mContext.getResources().obtainAttributes(attrs,
+                                R.styleable.UxRestrictions_StringRestrictions);
+                        mConfigBuilder.setMaxStringLength(a.getInt(
+                                R.styleable.UxRestrictions_StringRestrictions_maxLength,
+                                UX_RESTRICTIONS_UNKNOWN));
+
+                        break;
+                    case CONTENT_RESTRICTIONS:
+                        a = mContext.getResources().obtainAttributes(attrs,
+                                R.styleable.UxRestrictions_ContentRestrictions);
+                        mConfigBuilder.setMaxCumulativeContentItems(a.getInt(
+                                R.styleable.UxRestrictions_ContentRestrictions_maxCumulativeItems,
+                                UX_RESTRICTIONS_UNKNOWN));
+                        mConfigBuilder.setMaxContentDepth(a.getInt(
+                                R.styleable.UxRestrictions_ContentRestrictions_maxDepth,
+                                UX_RESTRICTIONS_UNKNOWN));
+                        break;
+                    default:
+                        if (Log.isLoggable(TAG, Log.DEBUG)) {
+                            Log.d(TAG, "Unsupported Restriction Parameters in XML: "
+                                    + parser.getName());
+                        }
+                        break;
+                }
+                if (a != null) {
+                    a.recycle();
+                }
+            }
+        }
+        return true;
+    }
+}
+
diff --git a/service/src/com/android/car/CarUxRestrictionsManagerService.java b/service/src/com/android/car/CarUxRestrictionsManagerService.java
index b90939b..868b825 100644
--- a/service/src/com/android/car/CarUxRestrictionsManagerService.java
+++ b/service/src/com/android/car/CarUxRestrictionsManagerService.java
@@ -16,32 +16,54 @@
 
 package com.android.car;
 
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+
 import android.annotation.Nullable;
+import android.car.Car;
 import android.car.drivingstate.CarDrivingStateEvent;
 import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState;
 import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
 import android.car.drivingstate.ICarDrivingStateChangeListener;
 import android.car.drivingstate.ICarUxRestrictionsChangeListener;
 import android.car.drivingstate.ICarUxRestrictionsManager;
 import android.car.hardware.CarPropertyValue;
 import android.car.hardware.property.CarPropertyEvent;
 import android.car.hardware.property.ICarPropertyEventListener;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.AsyncTask;
 import android.os.Binder;
 import android.os.Build;
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.AtomicFile;
+import android.util.JsonReader;
+import android.util.JsonWriter;
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import org.xmlpull.v1.XmlPullParserException;
 
+import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
@@ -49,6 +71,13 @@
 /**
  * A service that listens to current driving state of the vehicle and maps it to the
  * appropriate UX restrictions for that driving state.
+ * <p>
+ * <h1>UX Restrictions Configuration</h1>
+ * When this service starts, it will first try reading the configuration set through
+ * {@link #saveUxRestrictionsConfigurationForNextBoot(CarUxRestrictionsConfiguration)}.
+ * If one is not available, it will try reading the configuration saved in
+ * {@code R.xml.car_ux_restrictions_map}. If XML is somehow unavailable, it will
+ * fall back to a hard-coded configuration.
  */
 public class CarUxRestrictionsManagerService extends ICarUxRestrictionsManager.Stub implements
         CarServiceBase {
@@ -57,55 +86,187 @@
     private static final int MAX_TRANSITION_LOG_SIZE = 20;
     private static final int PROPERTY_UPDATE_RATE = 5; // Update rate in Hz
     private static final float SPEED_NOT_AVAILABLE = -1.0F;
+
+    @VisibleForTesting
+    /* package */ static final String CONFIG_FILENAME_PRODUCTION = "prod_config.json";
+    @VisibleForTesting
+    /* package */ static final String CONFIG_FILENAME_STAGED = "staged_config.json";
+
     private final Context mContext;
     private final CarDrivingStateService mDrivingStateService;
     private final CarPropertyService mCarPropertyService;
-    private final CarUxRestrictionsServiceHelper mHelper;
+    private final CarUserManagerHelper mCarUserManagerHelper;
     // List of clients listening to UX restriction events.
     private final List<UxRestrictionsClient> mUxRClients = new ArrayList<>();
+    private CarUxRestrictionsConfiguration mCarUxRestrictionsConfiguration;
     private CarUxRestrictions mCurrentUxRestrictions;
     private float mCurrentMovingSpeed;
-    private boolean mFallbackToDefaults;
     // Flag to disable broadcasting UXR changes - for development purposes
     @GuardedBy("this")
     private boolean mUxRChangeBroadcastEnabled = true;
     // For dumpsys logging
     private final LinkedList<Utils.TransitionLog> mTransitionLogs = new LinkedList<>();
 
+    // When UXR service boots up the context does not have access to storage yet, so it
+    // will likely read configuration from XML resource. Register to receive broadcast to
+    // attempt to read saved configuration when it becomes available.
+    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
+                // If the system user is not headless, then we can read config as soon as the
+                // system has completed booting.
+                if (!mCarUserManagerHelper.isHeadlessSystemUser()) {
+                    logd("not headless on boot complete");
+                    PendingResult pendingResult = goAsync();
+                    LoadRestrictionsTask task = new LoadRestrictionsTask(pendingResult);
+                    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+                }
+            } else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+                int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                logd("USER_SWITCHED: " + userHandle);
+                if (mCarUserManagerHelper.isHeadlessSystemUser()
+                        && userHandle > UserHandle.USER_SYSTEM) {
+                    PendingResult pendingResult = goAsync();
+                    LoadRestrictionsTask task = new LoadRestrictionsTask(pendingResult);
+                    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+                }
+            }
+        }
+    };
+
+    private class LoadRestrictionsTask extends AsyncTask<Void, Void, Void> {
+        private final BroadcastReceiver.PendingResult mPendingResult;
+
+        private LoadRestrictionsTask(BroadcastReceiver.PendingResult pendingResult) {
+            mPendingResult = pendingResult;
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            mCarUxRestrictionsConfiguration = loadConfig();
+            handleDispatchUxRestrictions(mDrivingStateService.getCurrentDrivingState().eventValue,
+                    getCurrentSpeed());
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void result) {
+            super.onPostExecute(result);
+            mPendingResult.finish();
+        }
+    }
 
     public CarUxRestrictionsManagerService(Context context, CarDrivingStateService drvService,
-            CarPropertyService propertyService) {
+            CarPropertyService propertyService, CarUserManagerHelper carUserManagerHelper) {
         mContext = context;
         mDrivingStateService = drvService;
         mCarPropertyService = propertyService;
-        mHelper = new CarUxRestrictionsServiceHelper(mContext, R.xml.car_ux_restrictions_map);
+        mCarUserManagerHelper = carUserManagerHelper;
+        // NOTE: during boot phase context cannot access file system so we most likely will
+        // use XML config. If prod config is set, it will be loaded in broadcast receiver.
+        mCarUxRestrictionsConfiguration = loadConfig();
         // Unrestricted until driving state information is received. During boot up, we don't want
         // everything to be blocked until data is available from CarPropertyManager.  If we start
         // driving and we don't get speed or gear information, we have bigger problems.
-        mCurrentUxRestrictions = mHelper.createUxRestrictionsEvent(false,
-                CarUxRestrictions.UX_RESTRICTIONS_BASELINE);
+        mCurrentUxRestrictions = new CarUxRestrictions.Builder(/* reqOpt= */ false,
+                CarUxRestrictions.UX_RESTRICTIONS_BASELINE, SystemClock.elapsedRealtimeNanos())
+                .build();
     }
 
     @Override
     public synchronized void init() {
-        try {
-            if (!mHelper.loadUxRestrictionsFromXml()) {
-                Log.e(TAG, "Error reading Ux Restrictions Mapping. Falling back to defaults");
-                mFallbackToDefaults = true;
-            }
-        } catch (IOException | XmlPullParserException e) {
-            Log.e(TAG, "Exception reading UX restrictions XML mapping", e);
-            mFallbackToDefaults = true;
-        }
         // subscribe to driving State
         mDrivingStateService.registerDrivingStateChangeListener(
                 mICarDrivingStateChangeEventListener);
         // subscribe to property service for speed
         mCarPropertyService.registerListener(VehicleProperty.PERF_VEHICLE_SPEED,
                 PROPERTY_UPDATE_RATE, mICarPropertyEventListener);
+        registerReceiverToLoadConfig();
         initializeUxRestrictions();
     }
 
+    private void registerReceiverToLoadConfig() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_USER_SWITCHED);
+        filter.addAction(Intent.ACTION_LOCKED_BOOT_COMPLETED);
+        mContext.registerReceiver(mBroadcastReceiver, filter);
+    }
+
+    @VisibleForTesting
+    @Nullable
+    /* package */ CarUxRestrictionsConfiguration getConfig() {
+        return mCarUxRestrictionsConfiguration;
+    }
+
+    /**
+     * Loads a UX restrictions configuration and returns it.
+     * <p>Reads config from the following sources in order:
+     * <ol>
+     * <li>saved config set by
+     * {@link #saveUxRestrictionsConfigurationForNextBoot(CarUxRestrictionsConfiguration)};
+     * <li>XML resource config from {@code R.xml.car_ux_restrictions_map};
+     * <li>hardcoded default config.
+     * </ol>
+     */
+    @VisibleForTesting
+    /* package */ synchronized CarUxRestrictionsConfiguration loadConfig() {
+        promoteStagedConfig();
+
+        CarUxRestrictionsConfiguration config = null;
+        // Production config, if available, is the first choice.
+        File prodConfig = mContext.getFileStreamPath(CONFIG_FILENAME_PRODUCTION);
+        if (prodConfig.exists()) {
+            logd("Attempting to read production config");
+            config = readPersistedConfig(prodConfig);
+            if (config != null) {
+                return config;
+            }
+        }
+
+        // XML config is the second choice.
+        logd("Attempting to read config from XML resource");
+        config = readXmlConfig();
+        if (config != null) {
+            return config;
+        }
+
+        // This should rarely happen.
+        Log.w(TAG, "Creating default config");
+        return createDefaultConfig();
+    }
+
+    @Nullable
+    private CarUxRestrictionsConfiguration readXmlConfig() {
+        try {
+            return CarUxRestrictionsConfigurationXmlParser.parse(mContext,
+                    R.xml.car_ux_restrictions_map);
+        } catch (IOException | XmlPullParserException e) {
+            Log.e(TAG, "Could not read config from XML resource", e);
+        }
+        return null;
+    }
+
+    private void promoteStagedConfig() {
+        Path stagedConfig = mContext.getFileStreamPath(CONFIG_FILENAME_STAGED).toPath();
+
+        CarDrivingStateEvent currentDrivingStateEvent =
+                mDrivingStateService.getCurrentDrivingState();
+        // Only promote staged config when car is parked.
+        if (currentDrivingStateEvent != null
+                && currentDrivingStateEvent.eventValue == CarDrivingStateEvent.DRIVING_STATE_PARKED
+                && Files.exists(stagedConfig)) {
+            Path prod = mContext.getFileStreamPath(CONFIG_FILENAME_PRODUCTION).toPath();
+            try {
+                logd("Attempting to promote stage config");
+                Files.move(stagedConfig, prod, REPLACE_EXISTING);
+            } catch (IOException e) {
+                Log.e(TAG, "Could not promote state config", e);
+            }
+        }
+    }
+
     // Update current restrictions by getting the current driving state and speed.
     private void initializeUxRestrictions() {
         CarDrivingStateEvent currentDrivingStateEvent =
@@ -159,9 +320,7 @@
     public synchronized void registerUxRestrictionsChangeListener(
             ICarUxRestrictionsChangeListener listener) {
         if (listener == null) {
-            if (DBG) {
-                Log.e(TAG, "registerUxRestrictionsChangeListener(): listener null");
-            }
+            Log.e(TAG, "registerUxRestrictionsChangeListener(): listener null");
             throw new IllegalArgumentException("Listener is null");
         }
         // If a new client is registering, create a new DrivingStateClient and add it to the list
@@ -232,6 +391,56 @@
         return mCurrentUxRestrictions;
     }
 
+    @Override
+    public synchronized boolean saveUxRestrictionsConfigurationForNextBoot(
+            CarUxRestrictionsConfiguration config) {
+        ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_UX_RESTRICTIONS_CONFIGURATION);
+        return persistConfig(config, CONFIG_FILENAME_STAGED);
+    }
+
+    /**
+     * Writes configuration into the specified file.
+     *
+     * IO access on file is not thread safe. Caller should ensure threading protection.
+     */
+    private boolean persistConfig(CarUxRestrictionsConfiguration config, String filename) {
+        AtomicFile stagedFile = new AtomicFile(mContext.getFileStreamPath(filename));
+        FileOutputStream fos;
+        try {
+            fos = stagedFile.startWrite();
+        } catch (IOException e) {
+            Log.e(TAG, "Could not open file to persist config", e);
+            return false;
+        }
+        try (JsonWriter jsonWriter = new JsonWriter(
+                new OutputStreamWriter(fos, StandardCharsets.UTF_8))) {
+            config.writeJson(jsonWriter);
+        } catch (IOException e) {
+            Log.e(TAG, "Could not persist config", e);
+            stagedFile.failWrite(fos);
+            return false;
+        }
+        stagedFile.finishWrite(fos);
+        return true;
+    }
+
+    @Nullable
+    private CarUxRestrictionsConfiguration readPersistedConfig(File file) {
+        if (!file.exists()) {
+            Log.e(TAG, "Could not find config file: " + file.getName());
+            return null;
+        }
+
+        AtomicFile config = new AtomicFile(file);
+        try (JsonReader reader = new JsonReader(
+                new InputStreamReader(config.openRead(), StandardCharsets.UTF_8))) {
+            return CarUxRestrictionsConfiguration.readJson(reader);
+        } catch (IOException e) {
+            Log.e(TAG, "Could not read persisted config file " + file.getName(), e);
+        }
+        return null;
+    }
+
     /**
      * Enable/disable UX restrictions change broadcast blocking.
      * Setting this to true will stop broadcasts of UX restriction change to listeners.
@@ -284,9 +493,7 @@
 
         @Override
         public void binderDied() {
-            if (DBG) {
-                Log.d(TAG, "Binder died " + listenerBinder);
-            }
+            logd("Binder died " + listenerBinder);
             listenerBinder.unlinkToDeath(this, 0);
             synchronized (CarUxRestrictionsManagerService.this) {
                 mUxRClients.remove(this);
@@ -315,9 +522,7 @@
             try {
                 listener.onUxRestrictionsChanged(event);
             } catch (RemoteException e) {
-                if (DBG) {
-                    Log.d(TAG, "Dispatch to listener failed");
-                }
+                Log.e(TAG, "Dispatch to listener failed", e);
             }
         }
     }
@@ -330,7 +535,7 @@
         if (isDebugBuild()) {
             writer.println("mUxRChangeBroadcastEnabled? " + mUxRChangeBroadcastEnabled);
         }
-        mHelper.dump(writer);
+        mCarUxRestrictionsConfiguration.dump(writer);
         writer.println("UX Restriction change log:");
         for (Utils.TransitionLog tlog : mTransitionLogs) {
             writer.println(tlog);
@@ -345,9 +550,7 @@
             new ICarDrivingStateChangeListener.Stub() {
                 @Override
                 public void onDrivingStateChanged(CarDrivingStateEvent event) {
-                    if (DBG) {
-                        Log.d(TAG, "Driving State Changed:" + event.eventValue);
-                    }
+                    logd("Driving State Changed:" + event.eventValue);
                     handleDrivingStateEvent(event);
                 }
             };
@@ -370,9 +573,7 @@
                 || drivingState == CarDrivingStateEvent.DRIVING_STATE_UNKNOWN) {
             // If speed is unavailable, but the driving state is parked or unknown, it can still be
             // handled.
-            if (DBG) {
-                Log.d(TAG, "Speed null when driving state is: " + drivingState);
-            }
+            logd("Speed null when driving state is: " + drivingState);
             mCurrentMovingSpeed = 0;
         } else {
             // If we get here with driving state != parked or unknown && speed == null,
@@ -430,14 +631,8 @@
             return;
         }
 
-        CarUxRestrictions uxRestrictions;
-        // Get UX restrictions from the parsed configuration XML or fall back to defaults if not
-        // available.
-        if (mFallbackToDefaults) {
-            uxRestrictions = getDefaultRestrictions(currentDrivingState);
-        } else {
-            uxRestrictions = mHelper.getUxRestrictions(currentDrivingState, speed);
-        }
+        CarUxRestrictions uxRestrictions =
+                mCarUxRestrictionsConfiguration.getUxRestrictions(currentDrivingState, speed);
 
         if (DBG) {
             Log.d(TAG, String.format("DO old->new: %b -> %b",
@@ -464,31 +659,23 @@
                 extraInfo.toString());
 
         mCurrentUxRestrictions = uxRestrictions;
-        if (DBG) {
-            Log.d(TAG, "dispatching to " + mUxRClients.size() + " clients");
-        }
+        logd("dispatching to " + mUxRClients.size() + " clients");
         for (UxRestrictionsClient client : mUxRClients) {
             client.dispatchEventToClients(uxRestrictions);
         }
     }
 
-    private CarUxRestrictions getDefaultRestrictions(@CarDrivingState int drivingState) {
-        int restrictions;
-        boolean requiresOpt = false;
-        switch (drivingState) {
-            case CarDrivingStateEvent.DRIVING_STATE_PARKED:
-                restrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
-                break;
-            case CarDrivingStateEvent.DRIVING_STATE_IDLING:
-                restrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
-                requiresOpt = false;
-                break;
-            case CarDrivingStateEvent.DRIVING_STATE_MOVING:
-            default:
-                restrictions = CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED;
-                requiresOpt = true;
-        }
-        return mHelper.createUxRestrictionsEvent(requiresOpt, restrictions);
+    CarUxRestrictionsConfiguration createDefaultConfig() {
+        return new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(CarDrivingStateEvent.DRIVING_STATE_PARKED,
+                      false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE)
+                .setUxRestrictions(CarDrivingStateEvent.DRIVING_STATE_IDLING,
+                      false, CarUxRestrictions.UX_RESTRICTIONS_BASELINE)
+                .setUxRestrictions(CarDrivingStateEvent.DRIVING_STATE_MOVING,
+                      true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED)
+                .setUxRestrictions(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN,
+                      true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED)
+                .build();
     }
 
     private void addTransitionLog(String name, int from, int to, long timestamp, String extra) {
@@ -500,4 +687,9 @@
         mTransitionLogs.add(tLog);
     }
 
+    private static void logd(String msg) {
+        if (DBG) {
+            Log.d(TAG, msg);
+        }
+    }
 }
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 736ffc8..e3381a3 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -111,13 +111,14 @@
         mSystemInterface = systemInterface;
         mHal = new VehicleHal(vehicle);
         mVehicleInterfaceName = vehicleInterfaceName;
+        mUserManagerHelper = new CarUserManagerHelper(serviceContext);
         mSystemActivityMonitoringService = new SystemActivityMonitoringService(serviceContext);
         mCarPowerManagementService = new CarPowerManagementService(mContext, mHal.getPowerHal(),
                 systemInterface);
         mCarPropertyService = new CarPropertyService(serviceContext, mHal.getPropertyHal());
         mCarDrivingStateService = new CarDrivingStateService(serviceContext, mCarPropertyService);
         mCarUXRestrictionsService = new CarUxRestrictionsManagerService(serviceContext,
-                mCarDrivingStateService, mCarPropertyService);
+                mCarDrivingStateService, mCarPropertyService, mUserManagerHelper);
         mCarPackageManagerService = new CarPackageManagerService(serviceContext,
                 mCarUXRestrictionsService,
                 mSystemActivityMonitoringService);
@@ -141,7 +142,6 @@
                 systemInterface);
         mCarConfigurationService =
                 new CarConfigurationService(serviceContext, new JsonReaderImpl());
-        mUserManagerHelper = new CarUserManagerHelper(serviceContext);
         mCarLocationService = new CarLocationService(
                 mContext, mCarPropertyService, mUserManagerHelper);
 
diff --git a/service/src/com/android/car/audio/CarAudioDeviceInfo.java b/service/src/com/android/car/audio/CarAudioDeviceInfo.java
index 2e35c56..f45f48e 100644
--- a/service/src/com/android/car/audio/CarAudioDeviceInfo.java
+++ b/service/src/com/android/car/audio/CarAudioDeviceInfo.java
@@ -38,6 +38,26 @@
  */
 /* package */ class CarAudioDeviceInfo {
 
+    /**
+     * Parse device address. Expected format is BUS%d_%s, address, usage hint
+     * @return valid address (from 0 to positive) or -1 for invalid address.
+     */
+    static int parseDeviceAddress(String address) {
+        String[] words = address.split("_");
+        int addressParsed = -1;
+        if (words[0].toLowerCase().startsWith("bus")) {
+            try {
+                addressParsed = Integer.parseInt(words[0].substring(3));
+            } catch (NumberFormatException e) {
+                //ignore
+            }
+        }
+        if (addressParsed < 0) {
+            return -1;
+        }
+        return addressParsed;
+    }
+
     private final AudioDeviceInfo mAudioDeviceInfo;
     private final int mBusNumber;
     private final int mSampleRate;
@@ -143,26 +163,6 @@
         }
     }
 
-    /**
-     * Parse device address. Expected format is BUS%d_%s, address, usage hint
-     * @return valid address (from 0 to positive) or -1 for invalid address.
-     */
-    private int parseDeviceAddress(String address) {
-        String[] words = address.split("_");
-        int addressParsed = -1;
-        if (words[0].toLowerCase().startsWith("bus")) {
-            try {
-                addressParsed = Integer.parseInt(words[0].substring(3));
-            } catch (NumberFormatException e) {
-                //ignore
-            }
-        }
-        if (addressParsed < 0) {
-            return -1;
-        }
-        return addressParsed;
-    }
-
     private int getMaxSampleRate(AudioDeviceInfo info) {
         int[] sampleRates = info.getSampleRates();
         if (sampleRates == null || sampleRates.length == 0) {
@@ -247,12 +247,12 @@
                 + " minGain: " + mMinGain;
     }
 
-    void dump(PrintWriter writer) {
-        writer.printf("Bus Number (%d) / address (%s)\n ",
-                mBusNumber, mAudioDeviceInfo.getAddress());
-        writer.printf("\tsample rate / encoding format / channel count: %d %d %d\n",
-                getSampleRate(), getEncodingFormat(), getChannelCount());
-        writer.printf("\tGain in millibel (min / max / default/ current): %d %d %d %d\n",
-                mMinGain, mMaxGain, mDefaultGain, mCurrentGain);
+    void dump(String indent, PrintWriter writer) {
+        writer.printf("%sCarAudioDeviceInfo Bus(%d: %s)\n ",
+                indent, mBusNumber, mAudioDeviceInfo.getAddress());
+        writer.printf("%s\tsample rate / encoding format / channel count: %d %d %d\n",
+                indent, getSampleRate(), getEncodingFormat(), getChannelCount());
+        writer.printf("%s\tGain values (min / max / default/ current): %d %d %d %d\n",
+                indent, mMinGain, mMaxGain, mDefaultGain, mCurrentGain);
     }
 }
diff --git a/service/src/com/android/car/audio/CarAudioDynamicRouting.java b/service/src/com/android/car/audio/CarAudioDynamicRouting.java
new file mode 100644
index 0000000..73b810c
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioDynamicRouting.java
@@ -0,0 +1,162 @@
+/*
+ * 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.audio;
+
+import android.hardware.automotive.audiocontrol.V1_0.ContextNumber;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
+import android.media.audiopolicy.AudioPolicy;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.car.CarLog;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Builds dynamic audio routing in a car from audio zone configuration.
+ */
+/* package */ class CarAudioDynamicRouting {
+
+    static final int[] CONTEXT_NUMBERS = new int[] {
+            ContextNumber.MUSIC,
+            ContextNumber.NAVIGATION,
+            ContextNumber.VOICE_COMMAND,
+            ContextNumber.CALL_RING,
+            ContextNumber.CALL,
+            ContextNumber.ALARM,
+            ContextNumber.NOTIFICATION,
+            ContextNumber.SYSTEM_SOUND
+    };
+
+    static final SparseIntArray USAGE_TO_CONTEXT = new SparseIntArray();
+
+    static final int DEFAULT_AUDIO_USAGE = AudioAttributes.USAGE_MEDIA;
+
+    // For legacy stream type based volume control.
+    // Values in STREAM_TYPES and STREAM_TYPE_USAGES should be aligned.
+    static final int[] STREAM_TYPES = new int[] {
+            AudioManager.STREAM_MUSIC,
+            AudioManager.STREAM_ALARM,
+            AudioManager.STREAM_RING
+    };
+    static final int[] STREAM_TYPE_USAGES = new int[] {
+            AudioAttributes.USAGE_MEDIA,
+            AudioAttributes.USAGE_ALARM,
+            AudioAttributes.USAGE_NOTIFICATION_RINGTONE
+    };
+
+    static {
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_UNKNOWN, ContextNumber.MUSIC);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_MEDIA, ContextNumber.MUSIC);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION, ContextNumber.CALL);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING,
+                ContextNumber.CALL);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ALARM, ContextNumber.ALARM);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION, ContextNumber.NOTIFICATION);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, ContextNumber.CALL_RING);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST,
+                ContextNumber.NOTIFICATION);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT,
+                ContextNumber.NOTIFICATION);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED,
+                ContextNumber.NOTIFICATION);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_EVENT, ContextNumber.NOTIFICATION);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY,
+                ContextNumber.VOICE_COMMAND);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
+                ContextNumber.NAVIGATION);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION,
+                ContextNumber.SYSTEM_SOUND);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_GAME, ContextNumber.MUSIC);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VIRTUAL_SOURCE, ContextNumber.INVALID);
+        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANT, ContextNumber.VOICE_COMMAND);
+    }
+
+    private final CarAudioZone[] mCarAudioZones;
+
+    CarAudioDynamicRouting(CarAudioZone[] carAudioZones) {
+        mCarAudioZones = carAudioZones;
+    }
+
+    void setupAudioDynamicRouting(AudioPolicy.Builder builder) {
+        for (CarAudioZone zone : mCarAudioZones) {
+            for (CarVolumeGroup group : zone.getVolumeGroups()) {
+                setupAudioDynamicRoutingForGroup(group, builder);
+            }
+        }
+    }
+
+    /**
+     * Enumerates all physical buses in a given volume group and attach the mixing rules.
+     * @param group {@link CarVolumeGroup} instance to enumerate the buses with
+     * @param builder {@link AudioPolicy.Builder} to attach the mixing rules
+     */
+    private void setupAudioDynamicRoutingForGroup(CarVolumeGroup group,
+            AudioPolicy.Builder builder) {
+        // Note that one can not register audio mix for same bus more than once.
+        for (int busNumber : group.getBusNumbers()) {
+            boolean hasContext = false;
+            CarAudioDeviceInfo info = group.getCarAudioDeviceInfoForBus(busNumber);
+            AudioFormat mixFormat = new AudioFormat.Builder()
+                    .setSampleRate(info.getSampleRate())
+                    .setEncoding(info.getEncodingFormat())
+                    .setChannelMask(info.getChannelCount())
+                    .build();
+            AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder();
+            for (int contextNumber : group.getContextsForBus(busNumber)) {
+                hasContext = true;
+                int[] usages = getUsagesForContext(contextNumber);
+                for (int usage : usages) {
+                    mixingRuleBuilder.addRule(
+                            new AudioAttributes.Builder().setUsage(usage).build(),
+                            AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
+                }
+                Log.d(CarLog.TAG_AUDIO, "Bus number: " + busNumber
+                        + " contextNumber: " + contextNumber
+                        + " sampleRate: " + info.getSampleRate()
+                        + " channels: " + info.getChannelCount()
+                        + " usages: " + Arrays.toString(usages));
+            }
+            if (hasContext) {
+                // It's a valid case that an audio output bus is defined in
+                // audio_policy_configuration and no context is assigned to it.
+                // In such case, do not build a policy mix with zero rules.
+                AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build())
+                        .setFormat(mixFormat)
+                        .setDevice(info.getAudioDeviceInfo())
+                        .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER)
+                        .build();
+                builder.addMix(audioMix);
+            }
+        }
+    }
+
+    private int[] getUsagesForContext(int contextNumber) {
+        final List<Integer> usages = new ArrayList<>();
+        for (int i = 0; i < CarAudioDynamicRouting.USAGE_TO_CONTEXT.size(); i++) {
+            if (CarAudioDynamicRouting.USAGE_TO_CONTEXT.valueAt(i) == contextNumber) {
+                usages.add(CarAudioDynamicRouting.USAGE_TO_CONTEXT.keyAt(i));
+            }
+        }
+        return usages.stream().mapToInt(i -> i).toArray();
+    }
+}
diff --git a/service/src/com/android/car/audio/CarAudioFocus.java b/service/src/com/android/car/audio/CarAudioFocus.java
new file mode 100644
index 0000000..c4aff05
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioFocus.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.audio;
+
+import android.hardware.automotive.audiocontrol.V1_0.ContextNumber;
+import android.media.AudioAttributes;
+import android.media.AudioFocusInfo;
+import android.media.AudioManager;
+import android.media.audiopolicy.AudioPolicy;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+
+public class CarAudioFocus extends AudioPolicy.AudioPolicyFocusListener {
+
+    private static final String TAG = "CarAudioFocus";
+
+    private final AudioManager  mAudioManager;
+    private CarAudioService     mCarAudioService;   // Dynamically assigned just after construction
+    private AudioPolicy         mAudioPolicy;       // Dynamically assigned just after construction
+
+
+    // Values for the internal interaction matrix we use to make focus decisions
+    private static final int INTERACTION_REJECT     = 0;    // Focus not granted
+    private static final int INTERACTION_EXCLUSIVE  = 1;    // Focus granted, others loose focus
+    private static final int INTERACTION_CONCURRENT = 2;    // Focus granted, others keep focus
+
+    // TODO:  Make this an overlayable resource...
+    //  MUSIC           = 1,        // Music playback
+    //  NAVIGATION      = 2,        // Navigation directions
+    //  VOICE_COMMAND   = 3,        // Voice command session
+    //  CALL_RING       = 4,        // Voice call ringing
+    //  CALL            = 5,        // Voice call
+    //  ALARM           = 6,        // Alarm sound from Android
+    //  NOTIFICATION    = 7,        // Notifications
+    //  SYSTEM_SOUND    = 8,        // User interaction sounds (button clicks, etc)
+    private static int sInteractionMatrix[][] = {
+        // Row selected by playing sound (labels along the right)
+        // Column selected by incoming request (labels along the top)
+        // Cell value is one of INTERACTION_REJECT, INTERACTION_EXCLUSIVE, INTERACTION_CONCURRENT
+        // Invalid, Music, Nav, Voice, Ring, Call, Alarm, Notification, System
+        {  0,       0,     0,   0,     0,    0,    0,     0,            0 }, // Invalid
+        {  0,       1,     2,   1,     1,    1,    1,     2,            2 }, // Music
+        {  0,       2,     2,   1,     2,    1,    2,     2,            2 }, // Nav
+        {  0,       2,     0,   2,     1,    1,    0,     0,            0 }, // Voice
+        {  0,       0,     2,   2,     2,    2,    0,     0,            2 }, // Ring
+        {  0,       0,     2,   0,     2,    2,    2,     2,            0 }, // Context
+        {  0,       2,     2,   1,     1,    1,    2,     2,            2 }, // Alarm
+        {  0,       2,     2,   1,     1,    1,    2,     2,            2 }, // Notification
+        {  0,       2,     2,   1,     1,    1,    2,     2,            2 }, // System
+    };
+
+
+    private class FocusEntry {
+        // Requester info
+        final AudioFocusInfo mAfi;                      // never null
+
+        final int mAudioContext;                        // Which HAL level context does this affect
+        final ArrayList<FocusEntry> mBlockers;          // List of requests that block ours
+
+        FocusEntry(AudioFocusInfo afi,
+                   int context) {
+            mAfi             = afi;
+            mAudioContext    = context;
+            mBlockers        = new ArrayList<FocusEntry>();
+        }
+
+        public String getClientId() {
+            return mAfi.getClientId();
+        }
+
+        public boolean wantsPauseInsteadOfDucking() {
+            return (mAfi.getFlags() & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0;
+        }
+    }
+
+
+    // We keep track of all the focus requesters in this map, with their clientId as the key.
+    // This is used both for focus dispatch and death handling
+    // Note that the clientId reflects the AudioManager instance and listener object (if any)
+    // so that one app can have more than one unique clientId by setting up distinct listeners.
+    // Because the listener gets only LOSS/GAIN messages, this is important for an app to do if
+    // it expects to request focus concurrently for different USAGEs so it knows which USAGE
+    // gained or lost focus at any given moment.  If the SAME listener is used for requests of
+    // different USAGE while the earlier request is still in the focus stack (whether holding
+    // focus or pending), the new request will be REJECTED so as to avoid any confusion about
+    // the meaning of subsequent GAIN/LOSS events (which would continue to apply to the focus
+    // request that was already active or pending).
+    private HashMap<String, FocusEntry> mFocusHolders = new HashMap<String, FocusEntry>();
+    private HashMap<String, FocusEntry> mFocusLosers = new HashMap<String, FocusEntry>();
+
+
+    CarAudioFocus(AudioManager audioManager) {
+        mAudioManager = audioManager;
+    }
+
+
+    // This has to happen after the construction to avoid a chicken and egg problem when setting up
+    // the AudioPolicy which must depend on this object.
+    public void setOwningPolicy(CarAudioService audioService, AudioPolicy parentPolicy) {
+        mCarAudioService = audioService;
+        mAudioPolicy     = parentPolicy;
+    }
+
+
+    // This sends a focus loss message to the targeted requester.
+    private void sendFocusLoss(FocusEntry loser, boolean permanent) {
+        int lossType = (permanent ? AudioManager.AUDIOFOCUS_LOSS :
+                                    AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+        Log.i(TAG, "sendFocusLoss to " + loser.getClientId());
+        int result = mAudioManager.dispatchAudioFocusChange(loser.mAfi, lossType, mAudioPolicy);
+        if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            // TODO:  Is this actually an error, or is it okay for an entry in the focus stack
+            // to NOT have a listener?  If that's the case, should we even keep it in the focus
+            // stack?
+            Log.e(TAG, "Failure to signal loss of audio focus with error: " + result);
+        }
+    }
+
+
+    /** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) */
+    // Note that we replicate most, but not all of the behaviors of the default MediaFocusControl
+    // engine as of Android P.
+    // Besides the interaction matrix which allows concurrent focus for multiple requestors, which
+    // is the reason for this module, we also treat repeated requests from the same clientId
+    // slightly differently.
+    // If a focus request for the same listener (clientId) is received while that listener is
+    // already in the focus stack, we REJECT it outright unless it is for the same USAGE.
+    // The default audio framework's behavior is to remove the previous entry in the stack (no-op
+    // if the requester is already holding focus).
+    int evaluateFocusRequest(AudioFocusInfo afi) {
+        Log.i(TAG, "Evaluating focus request for client " + afi.getClientId());
+
+        // Is this a request for premanant focus?
+        // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -- Means Notifications should be denied
+        // AUDIOFOCUS_GAIN_TRANSIENT -- Means current focus holders should get transient loss
+        // AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -- Means other can duck (no loss message from us)
+        // NOTE:  We expect that in practice it will be permanent for all media requests and
+        //        transient for everything else, but that isn't currently an enforced requirement.
+        final boolean permanent =
+                (afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN);
+        final boolean allowDucking =
+                (afi.getGainRequest() == AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
+
+
+        // Convert from audio attributes "usage" to HAL level "context"
+        final int requestedContext = mCarAudioService.getContextForUsage(
+                afi.getAttributes().getUsage());
+
+        // If we happen find an entry that this new request should replace, we'll store it here.
+        FocusEntry deprecatedBlockedEntry = null;
+
+        // Scan all active and pending focus requests.  If any should cause rejection of
+        // this new request, then we're done.  Keep a list of those against whom we're exclusive
+        // so we can update the relationships if/when we are sure we won't get rejected.
+        Log.i(TAG, "Scanning focus holders...");
+        final ArrayList<FocusEntry> losers = new ArrayList<FocusEntry>();
+        for (FocusEntry entry : mFocusHolders.values()) {
+            Log.i(TAG, entry.mAfi.getClientId());
+
+            // If this request is for Notifications and a current focus holder has specified
+            // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request.
+            // This matches the hardwired behavior in the default audio policy engine which apps
+            // might expect (The interaction matrix doesn't have any provision for dealing with
+            // override flags like this).
+            if ((requestedContext == ContextNumber.NOTIFICATION) &&
+                    (entry.mAfi.getGainRequest() ==
+                            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
+                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+            }
+
+            // We don't allow sharing listeners (client IDs) between two concurrent requests
+            // (because the app would have no way to know to which request a later event applied)
+            if (afi.getClientId().equals(entry.mAfi.getClientId())) {
+                if (entry.mAudioContext == requestedContext) {
+                    // Trivially accept if this request is a duplicate
+                    Log.i(TAG, "Duplicate request from focus holder is accepted");
+                    return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+                } else {
+                    // Trivially reject a request for a different USAGE
+                    Log.i(TAG, "Different request from focus holder is rejected");
+                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+                }
+            }
+
+            // Check the interaction matrix for the relationship between this entry and the request
+            switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) {
+                case INTERACTION_REJECT:
+                    // This request is rejected, so nothing further to do
+                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+                case INTERACTION_EXCLUSIVE:
+                    // The new request will cause this existing entry to lose focus
+                    losers.add(entry);
+                    break;
+                default:
+                    // If ducking isn't allowed by the focus requestor, then everybody else
+                    // must get a LOSS.
+                    // If a focus holder has set the AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS flag,
+                    // they must get a LOSS message even if ducking would otherwise be allowed.
+                    if ((!allowDucking) ||
+                            (entry.mAfi.getFlags() &
+                                    AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0) {
+                        // The new request will cause audio book to lose focus and pause
+                        losers.add(entry);
+                    }
+            }
+        }
+        Log.i(TAG, "Scanning those who've already lost focus...");
+        final ArrayList<FocusEntry> blocked = new ArrayList<FocusEntry>();
+        for (FocusEntry entry : mFocusLosers.values()) {
+            Log.i(TAG, entry.mAfi.getClientId());
+
+            // If this request is for Notifications and a pending focus holder has specified
+            // AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE, then reject the request
+            if ((requestedContext == ContextNumber.NOTIFICATION) &&
+                    (entry.mAfi.getGainRequest() ==
+                            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)) {
+                return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+            }
+
+            // We don't allow sharing listeners (client IDs) between two concurrent requests
+            // (because the app would have no way to know to which request a later event applied)
+            if (afi.getClientId().equals(entry.mAfi.getClientId())) {
+                if (entry.mAudioContext == requestedContext) {
+                    // This is a repeat of a request that is currently blocked.
+                    // Evaluate it as if it were a new request, but note that we should remove
+                    // the old pending request, and move it.
+                    // We do not want to evaluate the new request against itself.
+                    Log.i(TAG, "Duplicate request while waiting is being evaluated");
+                    deprecatedBlockedEntry = entry;
+                    continue;
+                } else {
+                    // Trivially reject a request for a different USAGE
+                    Log.i(TAG, "Different request while waiting is rejected");
+                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+                }
+            }
+
+            // Check the interaction matrix for the relationship between this entry and the request
+            switch (sInteractionMatrix[entry.mAudioContext][requestedContext]) {
+                case INTERACTION_REJECT:
+                    // Even though this entry has currently lost focus, the fact that it is
+                    // waiting to play means we'll reject this new conflicting request.
+                    return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+                case INTERACTION_EXCLUSIVE:
+                    // The new request is yet another reason this entry cannot regain focus (yet)
+                    blocked.add(entry);
+                    break;
+                default:
+                    // If ducking is not allowed by the requester, or the pending focus holder had
+                    // set the AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS flag,
+                    // then the pending holder must stay "lost" until this requester goes away.
+                    if ((!allowDucking) || entry.wantsPauseInsteadOfDucking()) {
+                        // The new request is yet another reason this entry cannot regain focus yet
+                        blocked.add(entry);
+                    }
+            }
+        }
+
+
+        // Now that we've decided we'll grant focus, construct our new FocusEntry
+        FocusEntry newEntry = new FocusEntry(afi, requestedContext);
+
+
+        // Now that we're sure we'll accept this request, update any requests which we would
+        // block but are already out of focus but waiting to come back
+        for (FocusEntry entry : blocked) {
+            // If we're out of focus it must be because somebody is blocking us
+            assert !entry.mBlockers.isEmpty();
+
+            if (permanent) {
+                // This entry has now lost focus forever
+                sendFocusLoss(entry, permanent);
+                final FocusEntry deadEntry = mFocusLosers.remove(entry.mAfi.getClientId());
+                assert deadEntry != null;
+            } else {
+                // Note that this new request is yet one more reason we can't (yet) have focus
+                entry.mBlockers.add(newEntry);
+            }
+        }
+
+        // Notify and update any requests which are now losing focus as a result of the new request
+        for (FocusEntry entry : losers) {
+            // If we have focus (but are about to loose it), nobody should be blocking us yet
+            assert entry.mBlockers.isEmpty();
+
+            sendFocusLoss(entry, permanent);
+
+            // The entry no longer holds focus, so take it out of the holders list
+            mFocusHolders.remove(entry.mAfi.getClientId());
+
+            if (!permanent) {
+                // Add ourselves to the list of requests waiting to get focus back and
+                // note why we lost focus so we can tell when it's time to get it back
+                mFocusLosers.put(entry.mAfi.getClientId(), entry);
+                entry.mBlockers.add(newEntry);
+            }
+        }
+
+        // If we encountered a duplicate of this request that was pending, but now we're going to
+        // grant focus, we need to remove the old pending request (without sending a LOSS message).
+        if (deprecatedBlockedEntry != null) {
+            mFocusLosers.remove(deprecatedBlockedEntry.mAfi.getClientId());
+        }
+
+        // Finally, add the request we're granting to the focus holders' list
+        mFocusHolders.put(afi.getClientId(), newEntry);
+
+        Log.i(TAG, "AUDIOFOCUS_REQUEST_GRANTED");
+        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+    }
+
+
+    @Override
+    public synchronized void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {
+        Log.i(TAG, "onAudioFocusRequest " + afi);
+
+        int response = evaluateFocusRequest(afi);
+
+        // Post our reply for delivery to the original focus requester
+        mAudioManager.setFocusRequestResult(afi, response, mAudioPolicy);
+    }
+
+
+    /**
+     * @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes)
+     * Note that we'll get this call for a focus holder that dies while in the focus statck, so
+     * we don't need to watch for death notifications directly.
+     * */
+    @Override
+    public synchronized void onAudioFocusAbandon(AudioFocusInfo afi) {
+        Log.i(TAG, "onAudioFocusAbandon " + afi);
+
+        // Remove this entry from our active or pending list
+        FocusEntry deadEntry = mFocusHolders.remove(afi.getClientId());
+        if (deadEntry == null) {
+            deadEntry = mFocusLosers.remove(afi.getClientId());
+            if (deadEntry == null) {
+                // Caller is providing an unrecognzied clientId!?
+                Log.w(TAG, "Audio focus abandoned by unrecognized client id: " + afi.getClientId());
+                // This probably means an app double released focused for some reason.  One
+                // harmless possibility is a race between an app being told it lost focus and the
+                // app voluntarily abandoning focus.  More likely the app is just sloppy.  :)
+                // The more nefarious possibility is that the clientId is actually corrupted
+                // somehow, in which case we might have a real focus entry that we're going to fail
+                // to remove. If that were to happen, I'd expect either the app to swallow it
+                // silently, or else take unexpected action (eg: resume playing spontaneously), or
+                // else to see "Failure to signal ..." gain/loss error messages in the log from
+                // this module when a focus change tries to take action on a truly zombie entry.
+            }
+        }
+
+        // Remove this entry from the blocking list of any pending requests
+        Iterator<FocusEntry> it = mFocusLosers.values().iterator();
+        while (it.hasNext()) {
+            FocusEntry entry = it.next();
+
+            // Remove the retiring entry from all blocker lists
+            entry.mBlockers.remove(deadEntry);
+
+            // Any entry whose blocking list becomes empty should regain focus
+            if (entry.mBlockers.isEmpty()) {
+                // Pull this entry out of the focus losers list
+                it.remove();
+
+                // Add it back into the focus holders list
+                mFocusHolders.put(entry.getClientId(), entry);
+
+                // Send the focus (re)gain notification
+                int result = mAudioManager.dispatchAudioFocusChange(
+                        entry.mAfi,
+                        entry.mAfi.getGainRequest(),
+                        mAudioPolicy);
+                if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+                    // TODO:  Is this actually an error, or is it okay for an entry in the focus
+                    // stack to NOT have a listener?  If that's the case, should we even keep
+                    // it in the focus stack?
+                    Log.e(TAG, "Failure to signal gain of audio focus with error: " + result);
+                }
+            }
+        }
+    }
+
+
+    public synchronized void dump(PrintWriter writer) {
+        writer.println("*CarAudioFocus*");
+
+        writer.println("  Current Focus Holders:");
+        for (String clientId : mFocusHolders.keySet()) {
+            System.out.println(clientId);
+        }
+
+        writer.println("  Transient Focus Losers:");
+        for (String clientId : mFocusLosers.keySet()) {
+            System.out.println(clientId);
+        }
+    }
+}
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index ebfe93e..324c5ac 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -27,7 +27,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.hardware.automotive.audiocontrol.V1_0.ContextNumber;
 import android.hardware.automotive.audiocontrol.V1_0.IAudioControl;
 import android.media.AudioAttributes;
 import android.media.AudioDeviceInfo;
@@ -40,8 +39,6 @@
 import android.media.AudioPlaybackConfiguration;
 import android.media.AudioPortConfig;
 import android.media.AudioSystem;
-import android.media.audiopolicy.AudioMix;
-import android.media.audiopolicy.AudioMixingRule;
 import android.media.audiopolicy.AudioPolicy;
 import android.os.IBinder;
 import android.os.Looper;
@@ -51,7 +48,6 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
-import android.util.SparseIntArray;
 import android.view.KeyEvent;
 
 import com.android.car.BinderInterfaceContainer;
@@ -63,7 +59,6 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashSet;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Set;
@@ -74,59 +69,30 @@
  */
 public class CarAudioService extends ICarAudio.Stub implements CarServiceBase {
 
-    private static final int DEFAULT_AUDIO_USAGE = AudioAttributes.USAGE_MEDIA;
+    // Turning this off will result in falling back to the default focus policy of Android
+    // (which boils down to "grant if not in a phone call, else deny").
+    // Aside from the obvious effect of ignoring the logic in CarAudioFocus, this will also
+    // result in the framework taking over responsibility for ducking in TRANSIENT_LOSS cases.
+    // Search for "DUCK_VSHAPE" in PLaybackActivityMonitor.java to see where this happens.
+    private static boolean sUseCarAudioFocus = true;
 
-    private static final int[] CONTEXT_NUMBERS = new int[] {
-            ContextNumber.MUSIC,
-            ContextNumber.NAVIGATION,
-            ContextNumber.VOICE_COMMAND,
-            ContextNumber.CALL_RING,
-            ContextNumber.CALL,
-            ContextNumber.ALARM,
-            ContextNumber.NOTIFICATION,
-            ContextNumber.SYSTEM_SOUND
-    };
+    // Key to persist master mute state in system settings
+    private static final String VOLUME_SETTINGS_KEY_MASTER_MUTE = "android.car.MASTER_MUTE";
 
-    private static final SparseIntArray USAGE_TO_CONTEXT = new SparseIntArray();
+    // The trailing slash forms a directory-liked hierarchy and
+    // allows listening for both GROUP/MEDIA and GROUP/NAVIGATION.
+    private static final String VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX = "android.car.VOLUME_GROUP/";
 
-    // For legacy stream type based volume control.
-    // Values in STREAM_TYPES and STREAM_TYPE_USAGES should be aligned.
-    private static final int[] STREAM_TYPES = new int[] {
-            AudioManager.STREAM_MUSIC,
-            AudioManager.STREAM_ALARM,
-            AudioManager.STREAM_RING
-    };
-    private static final int[] STREAM_TYPE_USAGES = new int[] {
-            AudioAttributes.USAGE_MEDIA,
-            AudioAttributes.USAGE_ALARM,
-            AudioAttributes.USAGE_NOTIFICATION_RINGTONE
-    };
-
-    static {
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_UNKNOWN, ContextNumber.MUSIC);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_MEDIA, ContextNumber.MUSIC);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION, ContextNumber.CALL);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING,
-                ContextNumber.CALL);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ALARM, ContextNumber.ALARM);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION, ContextNumber.NOTIFICATION);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_RINGTONE, ContextNumber.CALL_RING);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST,
-                ContextNumber.NOTIFICATION);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT,
-                ContextNumber.NOTIFICATION);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED,
-                ContextNumber.NOTIFICATION);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_NOTIFICATION_EVENT, ContextNumber.NOTIFICATION);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY,
-                ContextNumber.VOICE_COMMAND);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,
-                ContextNumber.NAVIGATION);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION,
-                ContextNumber.SYSTEM_SOUND);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_GAME, ContextNumber.MUSIC);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_VIRTUAL_SOURCE, ContextNumber.INVALID);
-        USAGE_TO_CONTEXT.put(AudioAttributes.USAGE_ASSISTANT, ContextNumber.VOICE_COMMAND);
+    /**
+     * Gets the key to persist volume for a volume group in settings
+     *
+     * @param zoneId The audio zone id
+     * @param groupId The volume group id
+     * @return Key to persist volume index for volume group in system settings
+     */
+    static String getVolumeSettingsKeyForGroup(int zoneId, int groupId) {
+        final int maskedGroupId = (zoneId << 8) + groupId;
+        return VOLUME_SETTINGS_KEY_FOR_GROUP_PREFIX + maskedGroupId;
     }
 
     private final Object mImplLock = new Object();
@@ -135,9 +101,8 @@
     private final TelephonyManager mTelephonyManager;
     private final AudioManager mAudioManager;
     private final boolean mUseDynamicRouting;
+    private final boolean mUseUnifiedConfiguration;
     private final boolean mPersistMasterMuteState;
-    private final SparseIntArray mContextToBus = new SparseIntArray();
-    private final SparseArray<CarAudioDeviceInfo> mCarAudioDeviceInfos = new SparseArray<>();
 
     private final AudioPolicy.AudioPolicyVolumeCallback mAudioPolicyVolumeCallback =
             new AudioPolicy.AudioPolicyVolumeCallback() {
@@ -147,29 +112,31 @@
             Log.v(CarLog.TAG_AUDIO,
                     "onVolumeAdjustment: " + AudioManager.adjustToString(adjustment)
                             + " suggested usage: " + AudioAttributes.usageToString(usage));
-            final int groupId = getVolumeGroupIdForUsage(usage);
-            final int currentVolume = getGroupVolume(groupId);
+            // TODO: Pass zone id into this callback.
+            final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
+            final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
+            final int currentVolume = getGroupVolume(zoneId, groupId);
             final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
             switch (adjustment) {
                 case AudioManager.ADJUST_LOWER:
-                    int minValue = Math.max(currentVolume - 1, getGroupMinVolume(groupId));
-                    setGroupVolume(groupId, minValue , flags);
+                    int minValue = Math.max(currentVolume - 1, getGroupMinVolume(zoneId, groupId));
+                    setGroupVolume(zoneId, groupId, minValue , flags);
                     break;
                 case AudioManager.ADJUST_RAISE:
-                    int maxValue =  Math.min(currentVolume + 1, getGroupMaxVolume(groupId));
-                    setGroupVolume(groupId, maxValue, flags);
+                    int maxValue =  Math.min(currentVolume + 1, getGroupMaxVolume(zoneId, groupId));
+                    setGroupVolume(zoneId, groupId, maxValue, flags);
                     break;
                 case AudioManager.ADJUST_MUTE:
                     setMasterMute(true, flags);
-                    callbackMasterMuteChange(flags);
+                    callbackMasterMuteChange(zoneId, flags);
                     break;
                 case AudioManager.ADJUST_UNMUTE:
                     setMasterMute(false, flags);
-                    callbackMasterMuteChange(flags);
+                    callbackMasterMuteChange(zoneId, flags);
                     break;
                 case AudioManager.ADJUST_TOGGLE_MUTE:
                     setMasterMute(!mAudioManager.isMasterMute(), flags);
-                    callbackMasterMuteChange(flags);
+                    callbackMasterMuteChange(zoneId, flags);
                     break;
                 case AudioManager.ADJUST_SAME:
                 default:
@@ -183,10 +150,12 @@
 
     /**
      * Simulates {@link ICarVolumeCallback} when it's running in legacy mode.
+     * This receiver assumes the intent is sent to {@link CarAudioManager#PRIMARY_AUDIO_ZONE}.
      */
     private final BroadcastReceiver mLegacyVolumeChangedReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
+            final int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
             switch (intent.getAction()) {
                 case AudioManager.VOLUME_CHANGED_ACTION:
                     int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
@@ -194,24 +163,27 @@
                     if (groupId == -1) {
                         Log.w(CarLog.TAG_AUDIO, "Unknown stream type: " + streamType);
                     } else {
-                        callbackGroupVolumeChange(groupId, 0);
+                        callbackGroupVolumeChange(zoneId, groupId, 0);
                     }
                     break;
                 case AudioManager.MASTER_MUTE_CHANGED_ACTION:
-                    callbackMasterMuteChange(0);
+                    callbackMasterMuteChange(zoneId, 0);
                     break;
             }
         }
     };
 
     private AudioPolicy mAudioPolicy;
-    private CarVolumeGroup[] mCarVolumeGroups;
+    private CarAudioFocus mFocusHandler;
+    private CarAudioZone[] mCarAudioZones;
 
     public CarAudioService(Context context) {
         mContext = context;
         mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
         mUseDynamicRouting = mContext.getResources().getBoolean(R.bool.audioUseDynamicRouting);
+        mUseUnifiedConfiguration = mContext.getResources().getBoolean(
+                R.bool.audioUseUnifiedConfiguration);
         mPersistMasterMuteState = mContext.getResources().getBoolean(
                 R.bool.audioPersistMasterMuteState);
     }
@@ -223,18 +195,38 @@
     @Override
     public void init() {
         synchronized (mImplLock) {
-            if (!mUseDynamicRouting) {
-                Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not configured, run in legacy mode");
-                setupLegacyVolumeChangedListener();
+            if (mUseDynamicRouting) {
+                // Enumerate all output bus device ports
+                AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(
+                        AudioManager.GET_DEVICES_OUTPUTS);
+                if (deviceInfos.length == 0) {
+                    Log.e(CarLog.TAG_AUDIO, "No output device available, ignore");
+                    return;
+                }
+                SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo = new SparseArray<>();
+                for (AudioDeviceInfo info : deviceInfos) {
+                    Log.v(CarLog.TAG_AUDIO, String.format("output id=%d address=%s type=%s",
+                            info.getId(), info.getAddress(), info.getType()));
+                    if (info.getType() == AudioDeviceInfo.TYPE_BUS) {
+                        final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(info);
+                        // See also the audio_policy_configuration.xml,
+                        // the bus number should be no less than zero.
+                        if (carInfo.getBusNumber() >= 0) {
+                            busToCarAudioDeviceInfo.put(carInfo.getBusNumber(), carInfo);
+                            Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo);
+                        }
+                    }
+                }
+                setupDynamicRouting(busToCarAudioDeviceInfo);
             } else {
-                setupDynamicRouting();
-                setupVolumeGroups();
+                Log.i(CarLog.TAG_AUDIO, "Audio dynamic routing not enabled, run in legacy mode");
+                setupLegacyVolumeChangedListener();
             }
 
             // Restore master mute state if applicable
             if (mPersistMasterMuteState) {
                 boolean storedMasterMute = Settings.Global.getInt(mContext.getContentResolver(),
-                        CarAudioManager.VOLUME_SETTINGS_KEY_MASTER_MUTE, 0) != 0;
+                        VOLUME_SETTINGS_KEY_MASTER_MUTE, 0) != 0;
                 setMasterMute(storedMasterMute, 0);
             }
         }
@@ -247,6 +239,8 @@
                 if (mAudioPolicy != null) {
                     mAudioManager.unregisterAudioPolicyAsync(mAudioPolicy);
                     mAudioPolicy = null;
+                    mFocusHandler.setOwningPolicy(null, null);
+                    mFocusHandler = null;
                 }
             } else {
                 mContext.unregisterReceiver(mLegacyVolumeChangedReceiver);
@@ -260,42 +254,44 @@
     public void dump(PrintWriter writer) {
         writer.println("*CarAudioService*");
         writer.println("\tRun in legacy mode? " + (!mUseDynamicRouting));
+        writer.println("\tUse unified configuration? " + mUseUnifiedConfiguration);
         writer.println("\tPersist master mute state? " + mPersistMasterMuteState);
         writer.println("\tMaster muted? " + mAudioManager.isMasterMute());
         // Empty line for comfortable reading
         writer.println();
         if (mUseDynamicRouting) {
-            for (CarVolumeGroup group : mCarVolumeGroups) {
-                group.dump(writer);
+            for (CarAudioZone zone : mCarAudioZones) {
+                zone.dump("\t", writer);
             }
         }
     }
 
     /**
-     * @see {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int)}
+     * @see {@link android.car.media.CarAudioManager#setGroupVolume(int, int, int, int)}
      */
     @Override
-    public void setGroupVolume(int groupId, int index, int flags) {
+    public void setGroupVolume(int zoneId, int groupId, int index, int flags) {
         synchronized (mImplLock) {
             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
 
-            callbackGroupVolumeChange(groupId, flags);
+            callbackGroupVolumeChange(zoneId, groupId, flags);
             // For legacy stream type based volume control
             if (!mUseDynamicRouting) {
-                mAudioManager.setStreamVolume(STREAM_TYPES[groupId], index, flags);
+                mAudioManager.setStreamVolume(
+                        CarAudioDynamicRouting.STREAM_TYPES[groupId], index, flags);
                 return;
             }
 
-            CarVolumeGroup group = getCarVolumeGroup(groupId);
+            CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
             group.setCurrentGainIndex(index);
         }
     }
 
-    private void callbackGroupVolumeChange(int groupId, int flags) {
+    private void callbackGroupVolumeChange(int zoneId, int groupId, int flags) {
         for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
                 mVolumeCallbackContainer.getInterfaces()) {
             try {
-                callback.binderInterface.onGroupVolumeChanged(groupId, flags);
+                callback.binderInterface.onGroupVolumeChanged(zoneId, groupId, flags);
             } catch (RemoteException e) {
                 Log.e(CarLog.TAG_AUDIO, "Failed to callback onGroupVolumeChanged", e);
             }
@@ -312,11 +308,11 @@
         mAudioManager.dispatchMediaKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keycode));
     }
 
-    private void callbackMasterMuteChange(int flags) {
+    private void callbackMasterMuteChange(int zoneId, int flags) {
         for (BinderInterfaceContainer.BinderInterface<ICarVolumeCallback> callback :
                 mVolumeCallbackContainer.getInterfaces()) {
             try {
-                callback.binderInterface.onMasterMuteChanged(flags);
+                callback.binderInterface.onMasterMuteChanged(zoneId, flags);
             } catch (RemoteException e) {
                 Log.e(CarLog.TAG_AUDIO, "Failed to callback onMasterMuteChanged", e);
             }
@@ -325,70 +321,73 @@
         // Persists master mute state if applicable
         if (mPersistMasterMuteState) {
             Settings.Global.putInt(mContext.getContentResolver(),
-                    CarAudioManager.VOLUME_SETTINGS_KEY_MASTER_MUTE,
+                    VOLUME_SETTINGS_KEY_MASTER_MUTE,
                     mAudioManager.isMasterMute() ? 1 : 0);
         }
     }
 
     /**
-     * @see {@link android.car.media.CarAudioManager#getGroupMaxVolume(int)}
+     * @see {@link android.car.media.CarAudioManager#getGroupMaxVolume(int, int)}
      */
     @Override
-    public int getGroupMaxVolume(int groupId) {
+    public int getGroupMaxVolume(int zoneId, int groupId) {
         synchronized (mImplLock) {
             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
 
             // For legacy stream type based volume control
             if (!mUseDynamicRouting) {
-                return mAudioManager.getStreamMaxVolume(STREAM_TYPES[groupId]);
+                return mAudioManager.getStreamMaxVolume(
+                        CarAudioDynamicRouting.STREAM_TYPES[groupId]);
             }
 
-            CarVolumeGroup group = getCarVolumeGroup(groupId);
+            CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
             return group.getMaxGainIndex();
         }
     }
 
     /**
-     * @see {@link android.car.media.CarAudioManager#getGroupMinVolume(int)}
+     * @see {@link android.car.media.CarAudioManager#getGroupMinVolume(int, int)}
      */
     @Override
-    public int getGroupMinVolume(int groupId) {
+    public int getGroupMinVolume(int zoneId, int groupId) {
         synchronized (mImplLock) {
             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
 
             // For legacy stream type based volume control
             if (!mUseDynamicRouting) {
-                return mAudioManager.getStreamMinVolume(STREAM_TYPES[groupId]);
+                return mAudioManager.getStreamMinVolume(
+                        CarAudioDynamicRouting.STREAM_TYPES[groupId]);
             }
 
-            CarVolumeGroup group = getCarVolumeGroup(groupId);
+            CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
             return group.getMinGainIndex();
         }
     }
 
     /**
-     * @see {@link android.car.media.CarAudioManager#getGroupVolume(int)}
+     * @see {@link android.car.media.CarAudioManager#getGroupVolume(int, int)}
      */
     @Override
-    public int getGroupVolume(int groupId) {
+    public int getGroupVolume(int zoneId, int groupId) {
         synchronized (mImplLock) {
             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
 
             // For legacy stream type based volume control
             if (!mUseDynamicRouting) {
-                return mAudioManager.getStreamVolume(STREAM_TYPES[groupId]);
+                return mAudioManager.getStreamVolume(
+                        CarAudioDynamicRouting.STREAM_TYPES[groupId]);
             }
 
-            CarVolumeGroup group = getCarVolumeGroup(groupId);
+            CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
             return group.getCurrentGainIndex();
         }
     }
 
-    private CarVolumeGroup getCarVolumeGroup(int groupId) {
-        Preconditions.checkNotNull(mCarVolumeGroups);
-        Preconditions.checkArgument(groupId >= 0 && groupId < mCarVolumeGroups.length,
-                "groupId out of range: " + groupId);
-        return mCarVolumeGroups[groupId];
+    private CarVolumeGroup getCarVolumeGroup(int zoneId, int groupId) {
+        Preconditions.checkNotNull(mCarAudioZones);
+        Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
+                "zoneId out of range: " + zoneId);
+        return mCarAudioZones[zoneId].getVolumeGroup(groupId);
     }
 
     private void setupLegacyVolumeChangedListener() {
@@ -398,186 +397,67 @@
         mContext.registerReceiver(mLegacyVolumeChangedReceiver, intentFilter);
     }
 
-    private void setupDynamicRouting() {
-        final IAudioControl audioControl = getAudioControl();
-        if (audioControl == null) {
-            return;
+    private void setupDynamicRouting(SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo) {
+        final AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
+        builder.setLooper(Looper.getMainLooper());
+
+        final CarAudioZonesLoader zonesLoader;
+        if (mUseUnifiedConfiguration) {
+            zonesLoader = new CarAudioZonesHelper(mContext, R.xml.car_audio_configuration,
+                    busToCarAudioDeviceInfo);
+        } else {
+            // In legacy mode, context -> bus mapping is done by querying IAudioControl HAL.
+            final IAudioControl audioControl = getAudioControl();
+            if (audioControl == null) {
+                throw new RuntimeException(
+                        "Dynamic routing requested but audioControl HAL not available");
+            }
+            zonesLoader = new CarAudioZonesHelperLegacy(mContext, R.xml.car_volume_groups,
+                    busToCarAudioDeviceInfo, audioControl);
         }
-        AudioPolicy audioPolicy = getDynamicAudioPolicy(audioControl);
-        int r = mAudioManager.registerAudioPolicy(audioPolicy);
+        mCarAudioZones = zonesLoader.loadAudioZones();
+        for (CarAudioZone zone : mCarAudioZones) {
+            if (!zone.validateVolumeGroups()) {
+                throw new RuntimeException("Invalid volume groups configuration");
+            }
+            // Ensure HAL gets our initial value
+            zone.synchronizeCurrentGainIndex();
+            Log.v(CarLog.TAG_AUDIO, "Processed audio zone: " + zone);
+        }
+
+        // Setup dynamic routing rules by usage
+        final CarAudioDynamicRouting dynamicRouting = new CarAudioDynamicRouting(mCarAudioZones);
+        dynamicRouting.setupAudioDynamicRouting(builder);
+
+        // Attach the {@link AudioPolicyVolumeCallback}
+        builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
+
+        if (sUseCarAudioFocus) {
+            // Configure our AudioPolicy to handle focus events.
+            // This gives us the ability to decide which audio focus requests to accept and bypasses
+            // the framework ducking logic.
+            mFocusHandler = new CarAudioFocus(mAudioManager);
+            builder.setAudioPolicyFocusListener(mFocusHandler);
+            builder.setIsAudioFocusPolicy(true);
+        }
+
+        mAudioPolicy = builder.build();
+        if (sUseCarAudioFocus) {
+            // Connect the AudioPolicy and the focus listener
+            mFocusHandler.setOwningPolicy(this, mAudioPolicy);
+        }
+
+        int r = mAudioManager.registerAudioPolicy(mAudioPolicy);
         if (r != AudioManager.SUCCESS) {
             throw new RuntimeException("registerAudioPolicy failed " + r);
         }
-        mAudioPolicy = audioPolicy;
-    }
-
-    private void setupVolumeGroups() {
-        Preconditions.checkArgument(mCarAudioDeviceInfos.size() > 0,
-                "No bus device is configured to setup volume groups");
-        final CarVolumeGroupsHelper helper = new CarVolumeGroupsHelper(
-                mContext, R.xml.car_volume_groups);
-        mCarVolumeGroups = helper.loadVolumeGroups();
-        for (CarVolumeGroup group : mCarVolumeGroups) {
-            for (int contextNumber : group.getContexts()) {
-                int busNumber = mContextToBus.get(contextNumber);
-                group.bind(contextNumber, busNumber, mCarAudioDeviceInfos.get(busNumber));
-            }
-
-            // Now that we have all our contexts, ensure the HAL gets our intial value
-            group.setCurrentGainIndex(group.getCurrentGainIndex());
-
-            Log.v(CarLog.TAG_AUDIO, "Processed volume group: " + group);
-        }
-        // Perform validation after all volume groups are processed
-        if (!validateVolumeGroups()) {
-            throw new RuntimeException("Invalid volume groups configuration");
-        }
     }
 
     /**
-     * Constraints applied here:
-     *
-     * - One context should not appear in two groups
-     * - All contexts are assigned
-     * - One bus should not appear in two groups
-     * - All gain controllers in the same group have same step value
-     *
-     * Note that it is fine that there are buses not appear in any group, those buses may be
-     * reserved for other usages.
-     * Step value validation is done in {@link CarVolumeGroup#bind(int, int, CarAudioDeviceInfo)}
-     *
-     * See also the car_volume_groups.xml configuration
+     * @return Context number for a given audio usage, 0 if the given usage is unrecognized.
      */
-    private boolean validateVolumeGroups() {
-        Set<Integer> contextSet = new HashSet<>();
-        Set<Integer> busNumberSet = new HashSet<>();
-        for (CarVolumeGroup group : mCarVolumeGroups) {
-            // One context should not appear in two groups
-            for (int context : group.getContexts()) {
-                if (contextSet.contains(context)) {
-                    Log.e(CarLog.TAG_AUDIO, "Context appears in two groups: " + context);
-                    return false;
-                }
-                contextSet.add(context);
-            }
-
-            // One bus should not appear in two groups
-            for (int busNumber : group.getBusNumbers()) {
-                if (busNumberSet.contains(busNumber)) {
-                    Log.e(CarLog.TAG_AUDIO, "Bus appears in two groups: " + busNumber);
-                    return false;
-                }
-                busNumberSet.add(busNumber);
-            }
-        }
-
-        // All contexts are assigned
-        if (contextSet.size() != CONTEXT_NUMBERS.length) {
-            Log.e(CarLog.TAG_AUDIO, "Some contexts are not assigned to group");
-            Log.e(CarLog.TAG_AUDIO, "Assigned contexts "
-                    + Arrays.toString(contextSet.toArray(new Integer[contextSet.size()])));
-            Log.e(CarLog.TAG_AUDIO, "All contexts " + Arrays.toString(CONTEXT_NUMBERS));
-            return false;
-        }
-
-        return true;
-    }
-
-    @Nullable
-    private AudioPolicy getDynamicAudioPolicy(@NonNull IAudioControl audioControl) {
-        AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
-        builder.setLooper(Looper.getMainLooper());
-
-        // 1st, enumerate all output bus device ports
-        AudioDeviceInfo[] deviceInfos = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
-        if (deviceInfos.length == 0) {
-            Log.e(CarLog.TAG_AUDIO, "getDynamicAudioPolicy, no output device available, ignore");
-            return null;
-        }
-        for (AudioDeviceInfo info : deviceInfos) {
-            Log.v(CarLog.TAG_AUDIO, String.format("output id=%d address=%s type=%s",
-                    info.getId(), info.getAddress(), info.getType()));
-            if (info.getType() == AudioDeviceInfo.TYPE_BUS) {
-                final CarAudioDeviceInfo carInfo = new CarAudioDeviceInfo(info);
-                // See also the audio_policy_configuration.xml and getBusForContext in
-                // audio control HAL, the bus number should be no less than zero.
-                if (carInfo.getBusNumber() >= 0) {
-                    mCarAudioDeviceInfos.put(carInfo.getBusNumber(), carInfo);
-                    Log.i(CarLog.TAG_AUDIO, "Valid bus found " + carInfo);
-                }
-            }
-        }
-
-        // 2nd, map context to physical bus
-        try {
-            for (int contextNumber : CONTEXT_NUMBERS) {
-                int busNumber = audioControl.getBusForContext(contextNumber);
-                mContextToBus.put(contextNumber, busNumber);
-                CarAudioDeviceInfo info = mCarAudioDeviceInfos.get(busNumber);
-                if (info == null) {
-                    Log.w(CarLog.TAG_AUDIO, "No bus configured for context: " + contextNumber);
-                }
-            }
-        } catch (RemoteException e) {
-            Log.e(CarLog.TAG_AUDIO, "Error mapping context to physical bus", e);
-        }
-
-        // 3rd, enumerate all physical buses and build the routing policy.
-        // Note that one can not register audio mix for same bus more than once.
-        for (int i = 0; i < mCarAudioDeviceInfos.size(); i++) {
-            int busNumber = mCarAudioDeviceInfos.keyAt(i);
-            boolean hasContext = false;
-            CarAudioDeviceInfo info = mCarAudioDeviceInfos.valueAt(i);
-            AudioFormat mixFormat = new AudioFormat.Builder()
-                    .setSampleRate(info.getSampleRate())
-                    .setEncoding(info.getEncodingFormat())
-                    .setChannelMask(info.getChannelCount())
-                    .build();
-            AudioMixingRule.Builder mixingRuleBuilder = new AudioMixingRule.Builder();
-            for (int j = 0; j < mContextToBus.size(); j++) {
-                if (mContextToBus.valueAt(j) == busNumber) {
-                    hasContext = true;
-                    int contextNumber = mContextToBus.keyAt(j);
-                    int[] usages = getUsagesForContext(contextNumber);
-                    for (int usage : usages) {
-                        mixingRuleBuilder.addRule(
-                                new AudioAttributes.Builder().setUsage(usage).build(),
-                                AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
-                    }
-                    Log.i(CarLog.TAG_AUDIO, "Bus number: " + busNumber
-                            + " contextNumber: " + contextNumber
-                            + " sampleRate: " + info.getSampleRate()
-                            + " channels: " + info.getChannelCount()
-                            + " usages: " + Arrays.toString(usages));
-                }
-            }
-            if (hasContext) {
-                // It's a valid case that an audio output bus is defined in
-                // audio_policy_configuration and no context is assigned to it.
-                // In such case, do not build a policy mix with zero rules.
-                AudioMix audioMix = new AudioMix.Builder(mixingRuleBuilder.build())
-                        .setFormat(mixFormat)
-                        .setDevice(info.getAudioDeviceInfo())
-                        .setRouteFlags(AudioMix.ROUTE_FLAG_RENDER)
-                        .build();
-                builder.addMix(audioMix);
-            }
-        }
-
-        // 4th, attach the {@link AudioPolicyVolumeCallback}
-        builder.setAudioPolicyVolumeCallback(mAudioPolicyVolumeCallback);
-
-        return builder.build();
-    }
-
-    private int[] getUsagesForContext(int contextNumber) {
-        final List<Integer> usages = new ArrayList<>();
-        for (int i = 0; i < USAGE_TO_CONTEXT.size(); i++) {
-            if (USAGE_TO_CONTEXT.valueAt(i) == contextNumber) {
-                usages.add(USAGE_TO_CONTEXT.keyAt(i));
-            }
-        }
-        return usages.stream().mapToInt(i -> i).toArray();
+    int getContextForUsage(int audioUsage) {
+        return CarAudioDynamicRouting.USAGE_TO_CONTEXT.get(audioUsage);
     }
 
     @Override
@@ -650,7 +530,7 @@
                 }
             }
 
-            return sourceAddresses.toArray(new String[sourceAddresses.size()]);
+            return sourceAddresses.toArray(new String[0]);
         }
     }
 
@@ -726,8 +606,9 @@
         Log.d(CarLog.TAG_AUDIO, "Audio patch created: " + patch[0]);
 
         // Ensure the initial volume on output device port
-        int groupId = getVolumeGroupIdForUsage(usage);
-        setGroupVolume(groupId, getGroupVolume(groupId), 0);
+        int groupId = getVolumeGroupIdForUsage(CarAudioManager.PRIMARY_AUDIO_ZONE, usage);
+        setGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groupId,
+                getGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groupId), 0);
 
         return new CarAudioPatchHandle(patch[0]);
     }
@@ -761,30 +642,30 @@
     }
 
     @Override
-    public int getVolumeGroupCount() {
+    public int getVolumeGroupCount(int zoneId) {
         synchronized (mImplLock) {
             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
-
             // For legacy stream type based volume control
-            if (!mUseDynamicRouting) return STREAM_TYPES.length;
+            if (!mUseDynamicRouting) return CarAudioDynamicRouting.STREAM_TYPES.length;
 
-            return mCarVolumeGroups == null ? 0 : mCarVolumeGroups.length;
+            Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
+                    "zoneId out of range: " + zoneId);
+            return mCarAudioZones[zoneId].getVolumeGroupCount();
         }
     }
 
     @Override
-    public int getVolumeGroupIdForUsage(@AudioAttributes.AttributeUsage int usage) {
+    public int getVolumeGroupIdForUsage(int zoneId, @AudioAttributes.AttributeUsage int usage) {
         synchronized (mImplLock) {
             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
+            Preconditions.checkArgumentInRange(zoneId, 0, mCarAudioZones.length - 1,
+                    "zoneId out of range: " + zoneId);
 
-            if (mCarVolumeGroups == null) {
-                return -1;
-            }
-
-            for (int i = 0; i < mCarVolumeGroups.length; i++) {
-                int[] contexts = mCarVolumeGroups[i].getContexts();
+            CarVolumeGroup[] groups = mCarAudioZones[zoneId].getVolumeGroups();
+            for (int i = 0; i < groups.length; i++) {
+                int[] contexts = groups[i].getContexts();
                 for (int context : contexts) {
-                    if (USAGE_TO_CONTEXT.get(usage) == context) {
+                    if (getContextForUsage(usage) == context) {
                         return i;
                     }
                 }
@@ -794,31 +675,28 @@
     }
 
     @Override
-    public @NonNull int[] getUsagesForVolumeGroupId(int groupId) {
+    public @NonNull int[] getUsagesForVolumeGroupId(int zoneId, int groupId) {
         synchronized (mImplLock) {
             enforcePermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME);
 
             // For legacy stream type based volume control
             if (!mUseDynamicRouting) {
-                return new int[] { STREAM_TYPE_USAGES[groupId] };
+                return new int[] { CarAudioDynamicRouting.STREAM_TYPE_USAGES[groupId] };
             }
 
-            CarVolumeGroup group = getCarVolumeGroup(groupId);
+            CarVolumeGroup group = getCarVolumeGroup(zoneId, groupId);
             Set<Integer> contexts =
                     Arrays.stream(group.getContexts()).boxed().collect(Collectors.toSet());
             final List<Integer> usages = new ArrayList<>();
-            for (int i = 0; i < USAGE_TO_CONTEXT.size(); i++) {
-                if (contexts.contains(USAGE_TO_CONTEXT.valueAt(i))) {
-                    usages.add(USAGE_TO_CONTEXT.keyAt(i));
+            for (int i = 0; i < CarAudioDynamicRouting.USAGE_TO_CONTEXT.size(); i++) {
+                if (contexts.contains(CarAudioDynamicRouting.USAGE_TO_CONTEXT.valueAt(i))) {
+                    usages.add(CarAudioDynamicRouting.USAGE_TO_CONTEXT.keyAt(i));
                 }
             }
             return usages.stream().mapToInt(i -> i).toArray();
         }
     }
 
-    /**
-     * See {@link android.car.media.CarAudioManager#registerVolumeCallback(IBinder)}
-     */
     @Override
     public void registerVolumeCallback(@NonNull IBinder binder) {
         synchronized (mImplLock) {
@@ -828,9 +706,6 @@
         }
     }
 
-    /**
-     * See {@link android.car.media.CarAudioManager#unregisterVolumeCallback(IBinder)}
-     */
     @Override
     public void unregisterVolumeCallback(@NonNull IBinder binder) {
         synchronized (mImplLock) {
@@ -852,11 +727,13 @@
      * Multiple usages may share one {@link AudioDevicePort}
      */
     private @Nullable AudioDevicePort getAudioPort(@AudioAttributes.AttributeUsage int usage) {
-        final int groupId = getVolumeGroupIdForUsage(usage);
-        final CarVolumeGroup group = Preconditions.checkNotNull(mCarVolumeGroups[groupId],
+        int zoneId = CarAudioManager.PRIMARY_AUDIO_ZONE;
+        final int groupId = getVolumeGroupIdForUsage(zoneId, usage);
+        final CarVolumeGroup group = Preconditions.checkNotNull(
+                mCarAudioZones[zoneId].getVolumeGroup(groupId),
                 "Can not find CarVolumeGroup by usage: "
                         + AudioAttributes.usageToString(usage));
-        return group.getAudioDevicePortForContext(USAGE_TO_CONTEXT.get(usage));
+        return group.getAudioDevicePortForContext(getContextForUsage(usage));
     }
 
     /**
@@ -879,7 +756,7 @@
                 return playbacks.get(playbacks.size() - 1).getAudioAttributes().getUsage();
             } else {
                 // TODO(b/72695246): Otherwise, get audio usage from foreground activity/window
-                return DEFAULT_AUDIO_USAGE;
+                return CarAudioDynamicRouting.DEFAULT_AUDIO_USAGE;
             }
         }
     }
@@ -891,8 +768,8 @@
      */
     private int getVolumeGroupIdForStreamType(int streamType) {
         int groupId = -1;
-        for (int i = 0; i < STREAM_TYPES.length; i++) {
-            if (streamType == STREAM_TYPES[i]) {
+        for (int i = 0; i < CarAudioDynamicRouting.STREAM_TYPES.length; i++) {
+            if (streamType == CarAudioDynamicRouting.STREAM_TYPES[i]) {
                 groupId = i;
                 break;
             }
@@ -911,4 +788,8 @@
         }
         return null;
     }
+
+    interface CarAudioZonesLoader {
+        CarAudioZone[] loadAudioZones();
+    }
 }
diff --git a/service/src/com/android/car/audio/CarAudioZone.java b/service/src/com/android/car/audio/CarAudioZone.java
new file mode 100644
index 0000000..b827906
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioZone.java
@@ -0,0 +1,146 @@
+/*
+ * 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.audio;
+
+import android.car.media.CarAudioManager;
+import android.util.Log;
+
+import com.android.car.CarLog;
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A class encapsulates an audio zone in car.
+ *
+ * An audio zone can contain multiple {@link CarVolumeGroup}s, and each zone has its own
+ * {@link CarAudioFocus} instance. Additionally, there may be dedicated hardware volume keys
+ * attached to each zone.
+ *
+ * See also the unified car_audio_configuration.xml
+ */
+/* package */ class CarAudioZone {
+
+    private final int mId;
+    private final String mName;
+    private final List<CarVolumeGroup> mVolumeGroups;
+
+    CarAudioZone(int id, String name) {
+        mId = id;
+        mName = name;
+        mVolumeGroups = new ArrayList<>();
+    }
+
+    int getId() {
+        return mId;
+    }
+
+    String getName() {
+        return mName;
+    }
+
+    boolean isPrimaryZone() {
+        return mId == CarAudioManager.PRIMARY_AUDIO_ZONE;
+    }
+
+    void addVolumeGroup(CarVolumeGroup volumeGroup) {
+        mVolumeGroups.add(volumeGroup);
+    }
+
+    CarVolumeGroup getVolumeGroup(int groupId) {
+        Preconditions.checkArgumentInRange(groupId, 0, mVolumeGroups.size() - 1,
+                "groupId(" + groupId + ") is out of range");
+        return mVolumeGroups.get(groupId);
+    }
+
+    int getVolumeGroupCount() {
+        return mVolumeGroups.size();
+    }
+
+    /**
+     * @return Snapshot of available {@link CarVolumeGroup}s in array.
+     */
+    CarVolumeGroup[] getVolumeGroups() {
+        return mVolumeGroups.toArray(new CarVolumeGroup[0]);
+    }
+
+    /**
+     * Constraints applied here:
+     *
+     * - One context should not appear in two groups
+     * - All contexts are assigned
+     * - One bus should not appear in two groups
+     * - All gain controllers in the same group have same step value
+     *
+     * Note that it is fine that there are buses not appear in any group, those buses may be
+     * reserved for other usages.
+     * Step value validation is done in {@link CarVolumeGroup#bind(int, int, CarAudioDeviceInfo)}
+     */
+    boolean validateVolumeGroups() {
+        Set<Integer> contextSet = new HashSet<>();
+        Set<Integer> busNumberSet = new HashSet<>();
+        for (CarVolumeGroup group : mVolumeGroups) {
+            // One context should not appear in two groups
+            for (int context : group.getContexts()) {
+                if (contextSet.contains(context)) {
+                    Log.e(CarLog.TAG_AUDIO, "Context appears in two groups: " + context);
+                    return false;
+                }
+                contextSet.add(context);
+            }
+
+            // One bus should not appear in two groups
+            for (int busNumber : group.getBusNumbers()) {
+                if (busNumberSet.contains(busNumber)) {
+                    Log.e(CarLog.TAG_AUDIO, "Bus appears in two groups: " + busNumber);
+                    return false;
+                }
+                busNumberSet.add(busNumber);
+            }
+        }
+
+        // All contexts are assigned
+        if (contextSet.size() != CarAudioDynamicRouting.CONTEXT_NUMBERS.length) {
+            Log.e(CarLog.TAG_AUDIO, "Some contexts are not assigned to group");
+            Log.e(CarLog.TAG_AUDIO, "Assigned contexts "
+                    + Arrays.toString(contextSet.toArray(new Integer[0])));
+            Log.e(CarLog.TAG_AUDIO,
+                    "All contexts " + Arrays.toString(CarAudioDynamicRouting.CONTEXT_NUMBERS));
+            return false;
+        }
+
+        return true;
+    }
+
+    void synchronizeCurrentGainIndex() {
+        for (CarVolumeGroup group : mVolumeGroups) {
+            group.setCurrentGainIndex(group.getCurrentGainIndex());
+        }
+    }
+
+    void dump(String indent, PrintWriter writer) {
+        writer.printf("%sCarAudioZone(%s:%d) isPrimary? %b\n", indent, mName, mId, isPrimaryZone());
+        for (CarVolumeGroup group : mVolumeGroups) {
+            group.dump(indent + "\t", writer);
+        }
+        writer.println();
+    }
+}
diff --git a/service/src/com/android/car/audio/CarAudioZonesHelper.java b/service/src/com/android/car/audio/CarAudioZonesHelper.java
new file mode 100644
index 0000000..ba7b93c
--- /dev/null
+++ b/service/src/com/android/car/audio/CarAudioZonesHelper.java
@@ -0,0 +1,202 @@
+/*
+ * 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.audio;
+
+import android.annotation.NonNull;
+import android.annotation.XmlRes;
+import android.car.media.CarAudioManager;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import com.android.car.CarLog;
+import com.android.car.R;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A helper class loads all audio zones from the configuration XML file.
+ */
+/* package */ class CarAudioZonesHelper implements CarAudioService.CarAudioZonesLoader {
+
+    private static final String TAG_ROOT = "carAudioConfiguration";
+    private static final String TAG_AUDIO_ZONES = "zones";
+    private static final String TAG_AUDIO_ZONE = "zone";
+    private static final String TAG_VOLUME_GROUPS = "volumeGroups";
+    private static final String TAG_VOLUME_GROUP = "group";
+    private static final String TAG_AUDIO_DEVICE = "device";
+    private static final String TAG_CONTEXT = "context";
+    private static final int SUPPORTED_VERSION = 1;
+
+    private final Context mContext;
+    private final int mXmlConfiguration;
+    private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo;
+
+    private int mNextSecondaryZoneId;
+
+    CarAudioZonesHelper(Context context, @XmlRes int xmlConfiguration,
+            @NonNull SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo) {
+        mContext = context;
+        mXmlConfiguration = xmlConfiguration;
+        mBusToCarAudioDeviceInfo = busToCarAudioDeviceInfo;
+
+        mNextSecondaryZoneId = CarAudioManager.PRIMARY_AUDIO_ZONE + 1;
+    }
+
+    @Override
+    public CarAudioZone[] loadAudioZones() {
+        List<CarAudioZone> carAudioZones = new ArrayList<>();
+        try (XmlResourceParser parser = mContext.getResources().getXml(mXmlConfiguration)) {
+            AttributeSet attrs = Xml.asAttributeSet(parser);
+            int type;
+            // Traverse to the first start tag, <carAudioConfiguration> in this case
+            while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+                    && type != XmlResourceParser.START_TAG) {
+                // ignored
+            }
+            if (!TAG_ROOT.equals(parser.getName())) {
+                throw new RuntimeException("Meta-data does not start with " + TAG_ROOT);
+            }
+
+            // Version check
+            TypedArray c = mContext.getResources().obtainAttributes(
+                    attrs, R.styleable.carAudioConfiguration);
+            final int versionNumber = c.getInt(R.styleable.carAudioConfiguration_version, -1);
+            if (versionNumber != SUPPORTED_VERSION) {
+                throw new RuntimeException("Support version:"
+                        + SUPPORTED_VERSION + " only, got version:" + versionNumber);
+            }
+            c.recycle();
+
+            // And follows with the <zones> tag
+            while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+                    && type != XmlResourceParser.START_TAG) {
+                // ignored
+            }
+            if (!TAG_AUDIO_ZONES.equals(parser.getName())) {
+                throw new RuntimeException("Configuration should begin with a <zones> tag");
+            }
+            int outerDepth = parser.getDepth();
+            while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+                    && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
+                if (type == XmlResourceParser.END_TAG) {
+                    continue;
+                }
+                if (TAG_AUDIO_ZONE.equals(parser.getName())) {
+                    carAudioZones.add(parseAudioZone(attrs, parser));
+                }
+            }
+        } catch (Exception e) {
+            Log.e(CarLog.TAG_AUDIO, "Error parsing unified car audio configuration", e);
+
+        }
+        return carAudioZones.toArray(new CarAudioZone[0]);
+    }
+
+    private CarAudioZone parseAudioZone(AttributeSet attrs, XmlResourceParser parser)
+            throws XmlPullParserException, IOException {
+        TypedArray c = mContext.getResources().obtainAttributes(
+                attrs, R.styleable.carAudioConfiguration);
+        final boolean isPrimary = c.getBoolean(R.styleable.carAudioConfiguration_isPrimary, false);
+        final String zoneName = c.getString(R.styleable.carAudioConfiguration_name);
+        c.recycle();
+
+        CarAudioZone zone = new CarAudioZone(
+                isPrimary ? CarAudioManager.PRIMARY_AUDIO_ZONE : getNextSecondaryZoneId(),
+                zoneName);
+        int type;
+        // Traverse to the first start tag, <volumeGroups> in this case
+        while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+                && type != XmlResourceParser.START_TAG) {
+            // ignored
+        }
+
+        if (!TAG_VOLUME_GROUPS.equals(parser.getName())) {
+            throw new RuntimeException("Audio zone does not start with <volumeGroups> tag");
+        }
+        int outerDepth = parser.getDepth();
+        int groupId = 0;
+        while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+                && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlResourceParser.END_TAG) {
+                continue;
+            }
+            if (TAG_VOLUME_GROUP.equals(parser.getName())) {
+                zone.addVolumeGroup(parseVolumeGroup(zone.getId(), groupId, attrs, parser));
+                groupId += 1;
+            }
+        }
+        return zone;
+    }
+
+    private CarVolumeGroup parseVolumeGroup(
+            int zoneId, int groupId, AttributeSet attrs, XmlResourceParser parser)
+            throws XmlPullParserException, IOException {
+        final CarVolumeGroup group = new CarVolumeGroup(mContext, zoneId, groupId);
+        int type;
+        int outerDepth = parser.getDepth();
+        while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+                && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlResourceParser.END_TAG) {
+                continue;
+            }
+            if (TAG_AUDIO_DEVICE.equals(parser.getName())) {
+                TypedArray c = mContext.getResources().obtainAttributes(
+                        attrs, R.styleable.carAudioConfiguration);
+                final String address = c.getString(R.styleable.carAudioConfiguration_address);
+                parseVolumeGroupContexts(group,
+                        CarAudioDeviceInfo.parseDeviceAddress(address), attrs, parser);
+                c.recycle();
+            }
+        }
+        return group;
+    }
+
+    private void parseVolumeGroupContexts(
+            CarVolumeGroup group, int busNumber, AttributeSet attrs, XmlResourceParser parser)
+            throws XmlPullParserException, IOException {
+        int type;
+        int innerDepth = parser.getDepth();
+        while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
+                && (type != XmlResourceParser.END_TAG || parser.getDepth() > innerDepth)) {
+            if (type == XmlResourceParser.END_TAG) {
+                continue;
+            }
+            if (TAG_CONTEXT.equals(parser.getName())) {
+                TypedArray c = mContext.getResources().obtainAttributes(
+                        attrs, R.styleable.volumeGroups_context);
+                final int contextNumber = c.getInt(
+                        R.styleable.volumeGroups_context_context, -1);
+                c.recycle();
+                group.bind(contextNumber, busNumber, mBusToCarAudioDeviceInfo.get(busNumber));
+            }
+        }
+    }
+
+    private int getNextSecondaryZoneId() {
+        int zoneId = mNextSecondaryZoneId;
+        mNextSecondaryZoneId += 1;
+        return zoneId;
+    }
+}
diff --git a/service/src/com/android/car/audio/CarVolumeGroupsHelper.java b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
similarity index 63%
rename from service/src/com/android/car/audio/CarVolumeGroupsHelper.java
rename to service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
index eddcd4f..fb5481d 100644
--- a/service/src/com/android/car/audio/CarVolumeGroupsHelper.java
+++ b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
@@ -15,12 +15,18 @@
  */
 package com.android.car.audio;
 
+import android.annotation.NonNull;
 import android.annotation.XmlRes;
+import android.car.media.CarAudioManager;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
+import android.hardware.automotive.audiocontrol.V1_0.IAudioControl;
+import android.os.RemoteException;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
 import android.util.Xml;
 
 import com.android.car.CarLog;
@@ -33,9 +39,12 @@
 import java.util.List;
 
 /**
- * A helper class loads all volume groups from the configuration XML file.
+ * A helper class loads volume groups from car_volume_groups.xml configuration into one zone.
+ *
+ * @deprecated This is replaced by {@link CarAudioZonesHelper}.
  */
-/* package */ class CarVolumeGroupsHelper {
+@Deprecated
+/* package */ class CarAudioZonesHelperLegacy implements CarAudioService.CarAudioZonesLoader {
 
     private static final String TAG_VOLUME_GROUPS = "volumeGroups";
     private static final String TAG_GROUP = "group";
@@ -43,16 +52,47 @@
 
     private final Context mContext;
     private final @XmlRes int mXmlConfiguration;
+    private final SparseIntArray mContextToBus;
+    private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo;
 
-    CarVolumeGroupsHelper(Context context, @XmlRes int xmlConfiguration) {
+    CarAudioZonesHelperLegacy(Context context, @XmlRes int xmlConfiguration,
+            @NonNull SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo,
+            @NonNull IAudioControl audioControl) {
         mContext = context;
         mXmlConfiguration = xmlConfiguration;
+        mBusToCarAudioDeviceInfo = busToCarAudioDeviceInfo;
+
+        // Initialize context => bus mapping once.
+        mContextToBus = new SparseIntArray();
+        try {
+            for (int contextNumber : CarAudioDynamicRouting.CONTEXT_NUMBERS) {
+                mContextToBus.put(contextNumber, audioControl.getBusForContext(contextNumber));
+            }
+        } catch (RemoteException e) {
+            Log.e(CarLog.TAG_AUDIO, "Failed to query IAudioControl HAL", e);
+            e.rethrowAsRuntimeException();
+        }
+    }
+
+    @Override
+    public CarAudioZone[] loadAudioZones() {
+        final CarAudioZone zone = new CarAudioZone(CarAudioManager.PRIMARY_AUDIO_ZONE,
+                "Primary zone");
+        for (CarVolumeGroup group : loadVolumeGroups()) {
+            zone.addVolumeGroup(group);
+            // Binding audio device to volume group.
+            for (int contextNumber : group.getContexts()) {
+                int busNumber = mContextToBus.get(contextNumber);
+                group.bind(contextNumber, busNumber, mBusToCarAudioDeviceInfo.get(busNumber));
+            }
+        }
+        return new CarAudioZone[] { zone };
     }
 
     /**
      * @return all {@link CarVolumeGroup} read from configuration.
      */
-    CarVolumeGroup[] loadVolumeGroups() {
+    private List<CarVolumeGroup> loadVolumeGroups() {
         List<CarVolumeGroup> carVolumeGroups = new ArrayList<>();
         try (XmlResourceParser parser = mContext.getResources().getXml(mXmlConfiguration)) {
             AttributeSet attrs = Xml.asAttributeSet(parser);
@@ -81,14 +121,13 @@
         } catch (Exception e) {
             Log.e(CarLog.TAG_AUDIO, "Error parsing volume groups configuration", e);
         }
-        return carVolumeGroups.toArray(new CarVolumeGroup[carVolumeGroups.size()]);
+        return carVolumeGroups;
     }
 
     private CarVolumeGroup parseVolumeGroup(int id, AttributeSet attrs, XmlResourceParser parser)
             throws XmlPullParserException, IOException {
-        int type;
-
         List<Integer> contexts = new ArrayList<>();
+        int type;
         int innerDepth = parser.getDepth();
         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
                 && (type != XmlResourceParser.END_TAG || parser.getDepth() > innerDepth)) {
@@ -103,7 +142,7 @@
             }
         }
 
-        return new CarVolumeGroup(mContext, id,
+        return new CarVolumeGroup(mContext, CarAudioManager.PRIMARY_AUDIO_ZONE, id,
                 contexts.stream().mapToInt(i -> i).filter(i -> i >= 0).toArray());
     }
 }
diff --git a/service/src/com/android/car/audio/CarVolumeGroup.java b/service/src/com/android/car/audio/CarVolumeGroup.java
index eff3b4c..eb3cc4f 100644
--- a/service/src/com/android/car/audio/CarVolumeGroup.java
+++ b/service/src/com/android/car/audio/CarVolumeGroup.java
@@ -29,7 +29,9 @@
 import com.android.internal.util.Preconditions;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 
 /**
  * A class encapsulates a volume group in car.
@@ -41,10 +43,10 @@
 /* package */ final class CarVolumeGroup {
 
     private final ContentResolver mContentResolver;
+    private final int mZoneId;
     private final int mId;
-    private final int[] mContexts;
     private final SparseIntArray mContextToBus = new SparseIntArray();
-    private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfos = new SparseArray<>();
+    private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo = new SparseArray<>();
 
     private int mDefaultGain = Integer.MIN_VALUE;
     private int mMaxGain = Integer.MIN_VALUE;
@@ -53,30 +55,78 @@
     private int mStoredGainIndex;
     private int mCurrentGainIndex = -1;
 
-    CarVolumeGroup(Context context, int id, @NonNull int[] contexts) {
+    /**
+     * Constructs a {@link CarVolumeGroup} instance
+     * @param context {@link Context} instance
+     * @param zoneId Audio zone this volume group belongs to
+     * @param id ID of this volume group
+     */
+    CarVolumeGroup(Context context, int zoneId, int id) {
         mContentResolver = context.getContentResolver();
+        mZoneId = zoneId;
         mId = id;
-        mContexts = contexts;
-
         mStoredGainIndex = Settings.Global.getInt(mContentResolver,
-                CarAudioManager.getVolumeSettingsKeyForGroup(mId), -1);
+                CarAudioService.getVolumeSettingsKeyForGroup(mZoneId, mId), -1);
     }
 
-    int getId() {
-        return mId;
+    /**
+     * Constructs a {@link CarVolumeGroup} instance
+     * @param context {@link Context} instance
+     * @param zoneId Audio zone this volume group belongs to
+     * @param id ID of this volume group
+     * @param contexts Pre-populated array of car contexts, for legacy car_volume_groups.xml only
+     * @deprecated In favor of {@link #CarVolumeGroup(Context, int, int)}
+     */
+    @Deprecated
+    CarVolumeGroup(Context context, int zoneId, int id, @NonNull int[] contexts) {
+        this(context, zoneId, id);
+        // Deal with the pre-populated car audio contexts
+        for (int audioContext : contexts) {
+            mContextToBus.put(audioContext, -1);
+        }
     }
 
+    /**
+     * @param busNumber Physical bus number for the audio device port
+     * @return {@link CarAudioDeviceInfo} associated with a given bus number
+     */
+    CarAudioDeviceInfo getCarAudioDeviceInfoForBus(int busNumber) {
+        return mBusToCarAudioDeviceInfo.get(busNumber);
+    }
+
+    /**
+     * @return Array of context numbers in this {@link CarVolumeGroup}
+     */
     int[] getContexts() {
-        return mContexts;
+        final int[] contextNumbers = new int[mContextToBus.size()];
+        for (int i = 0; i < contextNumbers.length; i++) {
+            contextNumbers[i] = mContextToBus.keyAt(i);
+        }
+        return contextNumbers;
+    }
+
+    /**
+     * @param busNumber Physical bus number for the audio device port
+     * @return Array of context numbers assigned to a given bus number
+     */
+    int[] getContextsForBus(int busNumber) {
+        List<Integer> contextNumbers = new ArrayList<>();
+        for (int i = 0; i < mContextToBus.size(); i++) {
+            int value = mContextToBus.valueAt(i);
+            if (value == busNumber) {
+                contextNumbers.add(mContextToBus.keyAt(i));
+            }
+        }
+        return contextNumbers.stream().mapToInt(i -> i).toArray();
     }
 
     /**
      * @return Array of bus numbers in this {@link CarVolumeGroup}
      */
     int[] getBusNumbers() {
-        final int[] busNumbers = new int[mBusToCarAudioDeviceInfos.size()];
+        final int[] busNumbers = new int[mBusToCarAudioDeviceInfo.size()];
         for (int i = 0; i < busNumbers.length; i++) {
-            busNumbers[i] = mBusToCarAudioDeviceInfos.keyAt(i);
+            busNumbers[i] = mBusToCarAudioDeviceInfo.keyAt(i);
         }
         return busNumbers;
     }
@@ -92,7 +142,7 @@
      * @param info {@link CarAudioDeviceInfo} instance relates to the physical bus
      */
     void bind(int contextNumber, int busNumber, CarAudioDeviceInfo info) {
-        if (mBusToCarAudioDeviceInfos.size() == 0) {
+        if (mBusToCarAudioDeviceInfo.size() == 0) {
             mStepSize = info.getAudioGain().stepValue();
         } else {
             Preconditions.checkArgument(
@@ -101,7 +151,7 @@
         }
 
         mContextToBus.put(contextNumber, busNumber);
-        mBusToCarAudioDeviceInfos.put(busNumber, info);
+        mBusToCarAudioDeviceInfo.put(busNumber, info);
 
         if (info.getDefaultGain() > mDefaultGain) {
             // We're arbitrarily selecting the highest bus default gain as the group's default.
@@ -156,14 +206,14 @@
                         + gainInMillibels + "index "
                         + gainIndex);
 
-        for (int i = 0; i < mBusToCarAudioDeviceInfos.size(); i++) {
-            CarAudioDeviceInfo info = mBusToCarAudioDeviceInfos.valueAt(i);
+        for (int i = 0; i < mBusToCarAudioDeviceInfo.size(); i++) {
+            CarAudioDeviceInfo info = mBusToCarAudioDeviceInfo.valueAt(i);
             info.setCurrentGain(gainInMillibels);
         }
 
         mCurrentGainIndex = gainIndex;
         Settings.Global.putInt(mContentResolver,
-                CarAudioManager.getVolumeSettingsKeyForGroup(mId), gainIndex);
+                CarAudioService.getVolumeSettingsKeyForGroup(mZoneId, mId), gainIndex);
     }
 
     // Given a group level gain index, return the computed gain in millibells
@@ -186,33 +236,35 @@
     @Nullable
     AudioDevicePort getAudioDevicePortForContext(int contextNumber) {
         final int busNumber = mContextToBus.get(contextNumber, -1);
-        if (busNumber < 0 || mBusToCarAudioDeviceInfos.get(busNumber) == null) {
+        if (busNumber < 0 || mBusToCarAudioDeviceInfo.get(busNumber) == null) {
             return null;
         }
-        return mBusToCarAudioDeviceInfos.get(busNumber).getAudioDevicePort();
+        return mBusToCarAudioDeviceInfo.get(busNumber).getAudioDevicePort();
     }
 
     @Override
     public String toString() {
         return "CarVolumeGroup id: " + mId
                 + " currentGainIndex: " + mCurrentGainIndex
-                + " contexts: " + Arrays.toString(mContexts)
+                + " contexts: " + Arrays.toString(getContexts())
                 + " buses: " + Arrays.toString(getBusNumbers());
     }
 
     /** Writes to dumpsys output */
-    void dump(PrintWriter writer) {
-        writer.println("CarVolumeGroup " + mId);
-        writer.printf("\tGain in millibel (min / max / default/ current): %d %d %d %d\n",
-                mMinGain, mMaxGain, mDefaultGain, getGainForIndex(mCurrentGainIndex));
-        writer.printf("\tGain in index (min / max / default / current): %d %d %d %d\n",
-                getMinGainIndex(), getMaxGainIndex(), getDefaultGainIndex(), mCurrentGainIndex);
+    void dump(String indent, PrintWriter writer) {
+        writer.printf("%sCarVolumeGroup(%d)\n", indent, mId);
+        writer.printf("%sGain values (min / max / default/ current): %d %d %d %d\n",
+                indent, mMinGain, mMaxGain,
+                mDefaultGain, getGainForIndex(mCurrentGainIndex));
+        writer.printf("%sGain indexes (min / max / default / current): %d %d %d %d\n",
+                indent, getMinGainIndex(), getMaxGainIndex(),
+                getDefaultGainIndex(), mCurrentGainIndex);
         for (int i = 0; i < mContextToBus.size(); i++) {
-            writer.printf("\tContext: %s -> Bus: %d\n",
+            writer.printf("%sContext: %s -> Bus: %d\n", indent,
                     ContextNumber.toString(mContextToBus.keyAt(i)), mContextToBus.valueAt(i));
         }
-        for (int i = 0; i < mBusToCarAudioDeviceInfos.size(); i++) {
-            mBusToCarAudioDeviceInfos.valueAt(i).dump(writer);
+        for (int i = 0; i < mBusToCarAudioDeviceInfo.size(); i++) {
+            mBusToCarAudioDeviceInfo.valueAt(i).dump(indent, writer);
         }
         // Empty line for comfortable reading
         writer.println();
diff --git a/tests/DirectRenderingClusterSample/AndroidManifest.xml b/tests/DirectRenderingClusterSample/AndroidManifest.xml
index be7026c..a5f117c 100644
--- a/tests/DirectRenderingClusterSample/AndroidManifest.xml
+++ b/tests/DirectRenderingClusterSample/AndroidManifest.xml
@@ -43,14 +43,15 @@
     <application android:label="@string/app_name"
                  android:icon="@mipmap/ic_launcher"
                  android:directBootAware="true">
-        <service android:name=".SampleClusterServiceImpl"
+        <service android:name=".ClusterRenderingServiceImpl"
                  android:exported="false"
                  android:singleUser="true"
                  android:permission="android.car.permission.BIND_INSTRUMENT_CLUSTER_RENDERER_SERVICE"/>
 
         <activity android:name=".MainClusterActivity"
             android:exported="false"
-            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+            android:showForAllUsers="true"
+            android:theme="@style/Theme.ClusterTheme">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.DEFAULT"/>
diff --git a/tests/DirectRenderingClusterSample/res/layout/activity_main.xml b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
index 393db54..2cb9662 100644
--- a/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
+++ b/tests/DirectRenderingClusterSample/res/layout/activity_main.xml
@@ -60,29 +60,12 @@
                 android:layout_width="20dp"
                 android:layout_height="1dp" />
 
-            <ImageView
-                android:id="@+id/maneuver"
-                android:layout_width="48dp"
-                android:layout_height="48dp"
-                android:layout_margin="10dp"
-                android:tint="@android:color/white"/>
+            <include
+                android:id="@+id/navigation_state"
+                layout="@layout/include_navigation_state"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"/>
 
-            <LinearLayout
-                android:layout_width="250dp"
-                android:layout_height="wrap_content"
-                android:orientation="vertical">
-
-                <TextView
-                    android:id="@+id/distance"
-                    android:layout_height="wrap_content"
-                    android:layout_width="wrap_content"
-                    android:textSize="30sp"/>
-                <TextView
-                    android:id="@+id/segment"
-                    android:layout_height="wrap_content"
-                    android:layout_width="wrap_content"
-                    android:textSize="18sp"/>
-            </LinearLayout>
         </LinearLayout>
     </LinearLayout>
 
diff --git a/tests/DirectRenderingClusterSample/res/layout/include_navigation_state.xml b/tests/DirectRenderingClusterSample/res/layout/include_navigation_state.xml
new file mode 100644
index 0000000..1946f0c
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/layout/include_navigation_state.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal">
+
+    <ImageView
+        android:id="@+id/maneuver"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_margin="10dp"
+        android:tint="@android:color/white"/>
+
+    <LinearLayout
+        android:layout_width="250dp"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/distance"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:textSize="30sp"/>
+        <TextView
+            android:id="@+id/segment"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:textSize="18sp"/>
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/res/values/styles.xml b/tests/DirectRenderingClusterSample/res/values/styles.xml
index f11f745..cd5f552 100644
--- a/tests/DirectRenderingClusterSample/res/values/styles.xml
+++ b/tests/DirectRenderingClusterSample/res/values/styles.xml
@@ -1,3 +1,5 @@
 <resources>
-
+    <style name="noAnimTheme" parent="android:Theme">
+        <item name="android:windowAnimationStyle">@null</item>
+    </style>
 </resources>
diff --git a/tests/DirectRenderingClusterSample/res/values/themes.xml b/tests/DirectRenderingClusterSample/res/values/themes.xml
new file mode 100644
index 0000000..972a036
--- /dev/null
+++ b/tests/DirectRenderingClusterSample/res/values/themes.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+-->
+<resources>
+    <style name="Theme.ClusterTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+        <item name="android:windowAnimationStyle">@null</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
similarity index 94%
rename from tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java
rename to tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
index f8a5b06..66d2d58 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/SampleClusterServiceImpl.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/ClusterRenderingServiceImpl.java
@@ -69,7 +69,7 @@
  * Implementation of {@link InstrumentClusterRenderingService} which renders an activity on a
  * virtual display that is transmitted to an external screen.
  */
-public class SampleClusterServiceImpl extends InstrumentClusterRenderingService {
+public class ClusterRenderingServiceImpl extends InstrumentClusterRenderingService {
     private static final String TAG = "Cluster.SampleService";
 
     private static final int NO_DISPLAY = -1;
@@ -103,7 +103,7 @@
         public void onDisplayAdded(int displayId) {
             Log.i(TAG, "Cluster display found, displayId: " + displayId);
             mDisplayId = displayId;
-            tryLaunchActivity();
+            launchMainActivity();
         }
 
         @Override
@@ -118,9 +118,9 @@
     };
 
     private static class UserReceiver extends BroadcastReceiver {
-        private WeakReference<SampleClusterServiceImpl> mService;
+        private WeakReference<ClusterRenderingServiceImpl> mService;
 
-        UserReceiver(SampleClusterServiceImpl service) {
+        UserReceiver(ClusterRenderingServiceImpl service) {
             mService = new WeakReference<>(service);
         }
 
@@ -136,16 +136,16 @@
 
         @Override
         public void onReceive(Context context, Intent intent) {
-            SampleClusterServiceImpl service = mService.get();
+            ClusterRenderingServiceImpl service = mService.get();
             Log.d(TAG, "Broadcast received: " + intent);
-            service.tryLaunchActivity();
+            service.tryLaunchNavigationActivity();
         }
     }
 
     private static class MessageHandler extends Handler {
-        private final WeakReference<SampleClusterServiceImpl> mService;
+        private final WeakReference<ClusterRenderingServiceImpl> mService;
 
-        MessageHandler(SampleClusterServiceImpl service) {
+        MessageHandler(ClusterRenderingServiceImpl service) {
             mService = new WeakReference<>(service);
         }
 
@@ -204,21 +204,13 @@
         mUserReceiver.register(this);
     }
 
-    private void tryLaunchActivity() {
-        int userHandle = ActivityManager.getCurrentUser();
-        if (userHandle == UserHandle.USER_SYSTEM || mDisplayId == NO_DISPLAY) {
-            Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)", userHandle,
-                    mDisplayId));
-            // Not ready to launch yet.
-            return;
-        }
+    private void launchMainActivity() {
         ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchDisplayId(mDisplayId);
         Intent intent = new Intent(this, MainClusterActivity.class);
         intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
-        startActivityAsUser(intent, options.toBundle(), UserHandle.of(userHandle));
-        Log.i(TAG, String.format("launching main activity: %s (user: %d, display: %d)", intent,
-                userHandle, mDisplayId));
+        startActivityAsUser(intent, options.toBundle(), UserHandle.SYSTEM);
+        Log.i(TAG, String.format("launching main activity: %s (display: %d)", intent, mDisplayId));
     }
 
     @Override
@@ -364,7 +356,7 @@
             }
             case "destroyOverlayDisplay": {
                 Settings.Global.putString(getContentResolver(),
-                    Global.OVERLAY_DISPLAY_DEVICES, "");
+                        Global.OVERLAY_DISPLAY_DEVICES, "");
                 break;
             }
 
@@ -407,6 +399,15 @@
      * have a default navigation activity selected yet.
      */
     private void tryLaunchNavigationActivity() {
+        int userHandle = ActivityManager.getCurrentUser();
+        if (userHandle == UserHandle.USER_SYSTEM || mNavigationDisplayId == NO_DISPLAY) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, String.format("Launch activity ignored (user: %d, display: %d)",
+                        userHandle, mNavigationDisplayId));
+            }
+            // Not ready to launch yet.
+            return;
+        }
         mHandler.removeCallbacks(mRetryLaunchNavigationActivity);
 
         Intent intent = getNavigationActivityIntent();
@@ -466,8 +467,7 @@
                     intent.setPackage(navigationApp.activityInfo.packageName);
                     intent.setComponent(new ComponentName(candidate.activityInfo.packageName,
                             candidate.activityInfo.name));
-                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                            | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                     return intent;
                 }
             }
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
index 2e38054..7464552 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/MainClusterActivity.java
@@ -16,17 +16,20 @@
 package android.car.cluster.sample;
 
 import static android.car.cluster.CarInstrumentClusterManager.CATEGORY_NAVIGATION;
-import static android.car.cluster.sample.SampleClusterServiceImpl.LOCAL_BINDING_ACTION;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_KEY_ACTIVITY_DISPLAY_ID;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_KEY_ACTIVITY_STATE;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_KEY_CATEGORY;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_KEY_KEY_EVENT;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_ON_KEY_EVENT;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_ON_NAVIGATION_STATE_CHANGED;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_REGISTER_CLIENT;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
-import static android.car.cluster.sample.SampleClusterServiceImpl.MSG_UNREGISTER_CLIENT;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.LOCAL_BINDING_ACTION;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_DISPLAY_ID;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_ACTIVITY_STATE;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_CATEGORY;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_KEY_KEY_EVENT;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_KEY_EVENT;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_ON_NAVIGATION_STATE_CHANGED;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_REGISTER_CLIENT;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_SET_ACTIVITY_LAUNCH_OPTIONS;
+import static android.car.cluster.sample.ClusterRenderingServiceImpl.MSG_UNREGISTER_CLIENT;
 
+import android.car.Car;
+import android.car.CarAppFocusManager;
+import android.car.CarNotConnectedException;
 import android.car.cluster.ClusterActivityState;
 import android.content.ComponentName;
 import android.content.Intent;
@@ -74,7 +77,8 @@
     private Messenger mService;
     private Messenger mServiceCallbacks = new Messenger(new MessageHandler(this));
     private VirtualDisplay mPendingVirtualDisplay = null;
-    private final Handler mHandler = new Handler();
+    private Car mCar;
+    private CarAppFocusManager mCarAppFocusManager;
 
     public static class VirtualDisplay {
         public final int mDisplayId;
@@ -96,7 +100,7 @@
         }
     };
 
-    private ServiceConnection mServiceConnection = new ServiceConnection() {
+    private ServiceConnection mClusterRenderingServiceConnection = new ServiceConnection() {
         @Override
         public void onServiceConnected(ComponentName name, IBinder service) {
             Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service);
@@ -117,6 +121,32 @@
         }
     };
 
+    private ServiceConnection mCarServiceConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            try {
+                Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service);
+                mCarAppFocusManager = (CarAppFocusManager) mCar.getCarManager(
+                        Car.APP_FOCUS_SERVICE);
+                if (mCarAppFocusManager == null) {
+                    Log.e(TAG, "onServiceConnected: unable to obtain CarAppFocusManager");
+                    return;
+                }
+                mCarAppFocusManager.addFocusListener((appType, active) -> {
+                    onNavigationFocusChanged(active);
+                }, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+            } catch (CarNotConnectedException e) {
+                Log.e(TAG, "onServiceConnected: error obtaining manager", e);
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.i(TAG, "onServiceDisconnected, name: " + name);
+            mCarAppFocusManager = null;
+        }
+    };
+
     private static class MessageHandler extends Handler {
         private final WeakReference<MainClusterActivity> mActivity;
 
@@ -138,7 +168,7 @@
                         data.setClassLoader(ParcelUtils.class.getClassLoader());
                         NavigationState navState = NavigationState
                                 .fromParcelable(data.getParcelable(
-                                        SampleClusterServiceImpl.NAV_STATE_BUNDLE_KEY));
+                                        ClusterRenderingServiceImpl.NAV_STATE_BUNDLE_KEY));
                         mActivity.get().onNavigationStateChange(navState);
                     }
                     break;
@@ -156,9 +186,9 @@
 
         mInputMethodManager = getSystemService(InputMethodManager.class);
 
-        Intent intent = new Intent(this, SampleClusterServiceImpl.class);
+        Intent intent = new Intent(this, ClusterRenderingServiceImpl.class);
         intent.setAction(LOCAL_BINDING_ACTION);
-        bindService(intent, mServiceConnection, 0);
+        bindService(intent, mClusterRenderingServiceConnection, 0);
 
         registerFacets(
                 new Facet<>(findViewById(R.id.btn_nav), 0, NavigationFragment.class),
@@ -169,19 +199,23 @@
         mPager = findViewById(R.id.pager);
         mPager.setAdapter(new ClusterPageAdapter(getSupportFragmentManager()));
         mOrderToFacet.get(0).button.requestFocus();
-        mNavStateController = new NavStateController(findViewById(R.id.maneuver),
-                findViewById(R.id.distance), findViewById(R.id.segment));
+        mNavStateController = new NavStateController(findViewById(R.id.navigation_state));
+
+        mCar = Car.createCar(this, mCarServiceConnection);
+        mCar.connect();
     }
 
     @Override
     protected void onDestroy() {
         super.onDestroy();
         Log.d(TAG, "onDestroy");
+        mCar.disconnect();
+        mCarAppFocusManager = null;
         if (mService != null) {
             sendServiceMessage(MSG_UNREGISTER_CLIENT, null, mServiceCallbacks);
             mService = null;
         }
-        unbindService(mServiceConnection);
+        unbindService(mClusterRenderingServiceConnection);
     }
 
     private void onKeyEvent(KeyEvent event) {
@@ -201,6 +235,12 @@
         }
     }
 
+    private void onNavigationFocusChanged(boolean active) {
+        if (mNavStateController != null) {
+            mNavStateController.setActive(active);
+        }
+    }
+
     public void updateNavDisplay(VirtualDisplay virtualDisplay) {
         if (mService == null) {
             // Service is not bound yet. Hold the information and notify when the service is bound.
@@ -223,8 +263,8 @@
     }
 
     /**
-     * Sends a message to the {@link SampleClusterServiceImpl}, which runs on a different process.
-
+     * Sends a message to the {@link ClusterRenderingServiceImpl}, which runs on a different
+     * process.
      * @param what action to perform
      * @param data action data
      * @param replyTo {@link Messenger} where to reply back
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
index b45a806..44b6268 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NavStateController.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.util.Log;
+import android.view.View;
 import android.widget.ImageView;
 import android.widget.TextView;
 
@@ -36,21 +37,21 @@
     private ImageView mManeuver;
     private TextView mDistance;
     private TextView mSegment;
+    private View mNavigationState;
     private Context mContext;
 
     /**
      * Creates a controller to coordinate updates to the views displaying navigation state
      * data.
      *
-     * @param maneuver {@link ImageView} used to display the immediate navigation maneuver
-     * @param distance {@link TextView} displaying distance to the maneuver
-     * @param segment {@link TextView} displaying the current street.
+     * @param container {@link View} containing the navigation state views
      */
-    public NavStateController(ImageView maneuver, TextView distance, TextView segment) {
-        mManeuver = maneuver;
-        mDistance = distance;
-        mSegment = segment;
-        mContext = maneuver.getContext();
+    public NavStateController(View container) {
+        mNavigationState = container;
+        mManeuver = container.findViewById(R.id.maneuver);
+        mDistance = container.findViewById(R.id.distance);
+        mSegment = container.findViewById(R.id.segment);
+        mContext = container.getContext();
     }
 
     /**
@@ -63,6 +64,18 @@
         mDistance.setText(formatDistance(step != null ? step.getDistance() : null));
     }
 
+    /**
+     * Updates whether turn-by-turn display is active or not. Turn-by-turn would be active whenever
+     * a navigation application has focus.
+     */
+    public void setActive(boolean active) {
+        Log.i(TAG, "Navigation status active: " + active);
+        if (!active) {
+            mManeuver.setImageDrawable(null);
+            mDistance.setText(null);
+        }
+    }
+
     private Drawable getManeuverIcon(@Nullable Maneuver maneuver) {
         if (maneuver == null) {
             return null;
diff --git a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
index a9c76a3..f5f535e 100644
--- a/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
+++ b/tests/DirectRenderingClusterSample/src/android/car/cluster/sample/NetworkedVirtualDisplay.java
@@ -68,7 +68,7 @@
 
     private static final int MSG_START = 0;
     private static final int MSG_STOP = 1;
-    private static final int MSG_RESUBMIT_FRAME = 2;
+    private static final int MSG_SEND_FRAME = 2;
 
     private VirtualDisplay mVirtualDisplay;
     private MediaCodec mVideoEncoder;
@@ -209,7 +209,7 @@
     }
 
     private void doOutputBufferAvailable(int index, @NonNull BufferInfo info) {
-        mHandler.removeMessages(MSG_RESUBMIT_FRAME);
+        mHandler.removeMessages(MSG_SEND_FRAME);
 
         ByteBuffer encodedData = mVideoEncoder.getOutputBuffer(index);
         if (encodedData == null) {
@@ -227,20 +227,17 @@
             encodedData.get(mBuffer, 0, mLastFrameLength);
             mVideoEncoder.releaseOutputBuffer(index, false);
 
-            sendFrame(mBuffer, mLastFrameLength);
-
-            // If nothing happens in Virtual Display we won't receive new frames. If we won't keep
-            // sending frames it could be a problem for the receiver because it needs certain
-            // number of frames in order to start decoding.
-            scheduleResendingLastFrame(1000 / FPS);
+            // Send this frame asynchronously (avoiding blocking on the socket). We might miss
+            // frames if the consumer is not fast enough, but this is acceptable.
+            sendFrameAsync(0);
         } else {
             Log.e(TAG, "Skipping empty buffer");
             mVideoEncoder.releaseOutputBuffer(index, false);
         }
     }
 
-    private void scheduleResendingLastFrame(long delayMs) {
-        Message msg = mHandler.obtainMessage(MSG_RESUBMIT_FRAME);
+    private void sendFrameAsync(long delayMs) {
+        Message msg = mHandler.obtainMessage(MSG_SEND_FRAME);
         mHandler.sendMessageDelayed(msg, delayMs);
     }
 
@@ -324,12 +321,12 @@
                     stopCasting();
                     break;
 
-                case MSG_RESUBMIT_FRAME:
+                case MSG_SEND_FRAME:
                     if (mServerSocket != null && mOutputStream != null) {
                         sendFrame(mBuffer, mLastFrameLength);
                     }
                     // We will keep sending last frame every second as a heartbeat.
-                    scheduleResendingLastFrame(1000L);
+                    sendFrameAsync(1000L);
                     break;
             }
         }
@@ -356,6 +353,9 @@
         try {
             Log.i(TAG, "Listening for incoming connections on port: " + PORT);
             Socket socket = serverSocket.accept();
+            socket.setTcpNoDelay(true);
+            socket.setKeepAlive(true);
+            socket.setSoLinger(true, 0);
 
             Log.i(TAG, "Receiver connected: " + socket);
             listenReceiverDisconnected(socket.getInputStream());
diff --git a/tests/EmbeddedKitchenSinkApp/Android.mk b/tests/EmbeddedKitchenSinkApp/Android.mk
index e6f457e..db59ddf 100644
--- a/tests/EmbeddedKitchenSinkApp/Android.mk
+++ b/tests/EmbeddedKitchenSinkApp/Android.mk
@@ -43,7 +43,6 @@
 LOCAL_STATIC_ANDROID_LIBRARIES += \
     car-service-lib-for-test \
     car-apps-common \
-    androidx.car_car \
     androidx.car_car-cluster
 
 LOCAL_STATIC_JAVA_LIBRARIES += \
diff --git a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_checked.xml b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_checked.xml
index d64d4fc..0ef8e43 100644
--- a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_checked.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_checked.xml
@@ -22,5 +22,5 @@
         android:viewportHeight="24.0">
     <path
         android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"
-        android:fillColor="#000000"/>
+        android:fillColor="#FFFFFF"/>
 </vector>
diff --git a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_unchecked.xml b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_unchecked.xml
index 96246a3..b05cb48 100644
--- a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_unchecked.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_check_box_unchecked.xml
@@ -22,5 +22,5 @@
         android:viewportHeight="24.0">
     <path
         android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"
-        android:fillColor="#000000"/>
+        android:fillColor="#FFFFFF"/>
 </vector>
diff --git a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_voice_assistant_mic.xml b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_voice_assistant_mic.xml
index 0468870..2171286 100644
--- a/tests/EmbeddedKitchenSinkApp/res/drawable/ic_voice_assistant_mic.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/drawable/ic_voice_assistant_mic.xml
@@ -21,7 +21,7 @@
  android:viewportWidth="24"
  android:viewportHeight="24">
  <path
-  android:fillColor="#000000"
+  android:fillColor="#FFFFFF"
   android:pathData="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3
 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6
 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z" />
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml
index 15422b1..cdf437e 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/connectivity_fragment.xml
@@ -19,37 +19,68 @@
               android:layout_width="match_parent"
               android:layout_height="match_parent">
 
+    <!-- List(s) of networks (right now only have view all, may do a search UI)
+    -->
     <LinearLayout
-            android:orientation="horizontal"
+            android:orientation="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+        <!-- Header for table -->
+        <LinearLayout
+                android:orientation="horizontal"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingLeft="4dp"
+                android:background="#3C3F41"
+                android:weightSum="12">
+
+            <TextView
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:textSize="32sp"
+                android:textColor="#d4d4d4"
+                android:textStyle="bold"
+                android:text="Net ID">
+            </TextView>
+
+            <TextView
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="9"
+                android:textSize="32sp"
+                android:textColor="#d4d4d4"
+                android:textStyle="bold"
+                android:text="Details">
+            </TextView>
+
+            <TextView
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="2"
+                android:textSize="32sp"
+                android:textColor="#d4d4d4"
+                android:textStyle="bold"
+                android:text="Functions">
+            </TextView>
+
+        </LinearLayout>
+
+        <!-- Wrapped list view to implement swipe to refresh -->
+        <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+            android:id="@+id/refreshNetworksList"
             android:layout_width="match_parent"
             android:layout_height="wrap_content">
-        <ListView
-                android:id="@+id/networks"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content">
-        </ListView>
-    </LinearLayout>
-    <LinearLayout
-            android:orientation="horizontal"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_margin="4dp">
-        <Button android:id="@+id/networksRefresh"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="Refresh"/>
-        <Button android:id="@+id/networkRequestOemPaid"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="Request OEM-paid"/>
-        <Button android:id="@+id/networkRequestEth1"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="Request eth1"/>
-        <Button android:id="@+id/networkReleaseNetwork"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="Release Request"/>
-    </LinearLayout>
 
-</LinearLayout>
\ No newline at end of file
+            <!-- Filled in code with network_item.xml -->
+            <ListView
+                    android:id="@+id/networks"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+            </ListView>
+
+        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+
+    </LinearLayout>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml b/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml
index 45b38dc..2ebc1c6 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/kitchen_content.xml
@@ -17,7 +17,6 @@
 <!-- We use this container to place kitchen app fragments. It insets the fragment contents -->
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/kitchen_content"
-    android:background="#A8A9AA"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:paddingStart="56dp"
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/network_item.xml b/tests/EmbeddedKitchenSinkApp/res/layout/network_item.xml
new file mode 100644
index 0000000..84d9d34
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/network_item.xml
@@ -0,0 +1,203 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- 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.
+-->
+
+<!-- Used for the list of networks in the KS Connectivity Tab -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_gravity="fill_vertical"
+    android:orientation="horizontal"
+    android:weightSum="12">
+
+    <!-- Requested Active indicator -->
+    <View
+        android:id="@+id/network_active"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight=".1"
+        android:background="#ff3d3d">
+    </View>
+
+    <!-- Item NetId indicator -->
+    <TextView
+        android:id="@+id/network_id"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight=".9"
+        android:textSize="20sp"
+        android:textColor="#d4d4d4">
+    </TextView>
+
+    <!-- Set of network values we want to view -->
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_weight="9">
+
+        <TextView
+            android:id="@+id/network_type"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_state"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_connected"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_available"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_roaming"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_iface"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/hw_address"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_ip_addresses"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_dns"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_domains"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_routes"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_transports"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_capabilities"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+        <TextView
+            android:id="@+id/network_bandwidth"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textSize="20sp"
+            android:textColor="#d4d4d4">
+        </TextView>
+
+    </LinearLayout>
+
+    <!-- Buttons to trigger a request or something else -->
+    <LinearLayout
+            android:orientation="vertical"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="2"
+            android:weightSum="3">
+
+        <Button android:id="@+id/network_request"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:layout_marginBottom="2dp"
+                android:background="#505050"
+                android:text="Request"
+                android:textColor="#d4d4d4"/>
+
+        <Button android:id="@+id/network_default"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:layout_marginBottom="2dp"
+                android:background="#505050"
+                android:text="Set Default"
+                android:textColor="#d4d4d4"/>
+
+        <Button android:id="@+id/network_report"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:layout_marginBottom="2dp"
+                android:background="#505050"
+                android:text="Report"
+                android:textColor="#d4d4d4"/>
+
+    </LinearLayout>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
index b9b8e8b..1dc40f6 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
@@ -65,4 +65,21 @@
         android:layout_height="wrap_content"
         android:text="Category: CATEGORY_MESSAGE"
         android:textSize="35sp"/>
+    <LinearLayout
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal" >
+         <Button
+        android:id="@+id/category_car_emerg_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Category: CATEGORY_EMERG"
+        android:textSize="35sp"/>
+         <Button
+        android:id="@+id/category_car_warning_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Category: CATEGORY_WARN"
+        android:textSize="35sp"/>
+    </LinearLayout>
 </LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml b/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
index 4d8c246..af1866a 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/sensors.xml
@@ -23,7 +23,7 @@
             android:layout_height="110dp"
             android:orientation="vertical">
         <TextView
-            android:id="@string/location_title"
+            android:id="@+id/location_title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="@string/location_title" />
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml b/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml
index cf98695..52a7763 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml
@@ -23,7 +23,6 @@
         android:layout_height="wrap_content"
         android:text="Storage Wear Information"
         android:textSize="24dp"
-        android:textColor="#ff0000"
         android:minLines="5"/>
     <ListView
         android:id="@+id/storage_events_list"
@@ -58,7 +57,6 @@
         android:layout_height="wrap_content"
         android:text="Free Disk Space: 1 byte"
         android:textSize="24dp"
-        android:textColor="#ff0000"
         android:minLines="4"/>
     <ScrollView
         android:id="@+id/scroll_view"
@@ -72,7 +70,6 @@
             android:layout_height="fill_parent"
             android:text="No I/O activity on record"
             android:textSize="20dp"
-            android:textColor="#ff0000"
             android:minLines="10"/>
     </ScrollView>
 </LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/weblinks_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/weblinks_fragment.xml
new file mode 100644
index 0000000..6200ddc
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/weblinks_fragment.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginStart="40dp"
+    android:layout_marginEnd="40dp"
+    android:id="@+id/buttons">
+    <Button
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:layout_marginTop="40dp"
+        android:text="@string/weblink_google"
+        android:tag="@string/weblink_google" />
+    <Button
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:layout_marginTop="40dp"
+        android:text="@string/weblink_nytimes"
+        android:tag="@string/weblink_nytimes" />
+    <Button
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:layout_marginTop="40dp"
+        android:text="@string/weblink_support_name"
+        android:tag="@string/weblink_support" />
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index e8ac7d3..35d3ec2 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -293,4 +293,9 @@
     <!-- Virtual Display -->
     <string name="av_start_activity">Start Activity</string>
     <string name="av_resize">Resize</string>
+
+    <string name="weblink_google" translatable="false">www.google.com</string>
+    <string name="weblink_nytimes" translatable="false">www.nytimes.com</string>
+    <string name="weblink_support_name" translatable="false">support.google.com</string>
+    <string name="weblink_support" translatable="false">https://support.google.com/chrome/answer/95414?hl=en&amp;ref_topic=7438008</string>
 </resources>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/styles.xml b/tests/EmbeddedKitchenSinkApp/res/values/styles.xml
index f16d19d..1dcaa55 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/styles.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/styles.xml
@@ -30,13 +30,10 @@
         <item name="android:layout_width">@dimen/overview_icon_size</item>
         <item name="android:layout_height">@dimen/overview_icon_size</item>
         <item name="android:padding">6dp</item>
-        <item name="android:background">?android:attr/selectableItemBackgroundBorderless</item>
-        <item name="android:tint">@color/car_button_tint</item>
         <item name="android:scaleType">fitCenter</item>
         <item name="android:clickable">true</item>
     </style>
 
-    <style name="KitchenSinkActivityTheme" parent="Theme.Car.Light.NoActionBar.Drawer">
-        <item name="android:colorPrimary">@android:color/transparent</item>
+    <style name="KitchenSinkActivityTheme" parent="Theme.NoActionBar.Drawer">
     </style>
 </resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index 3b8b435..acc651d 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -33,11 +33,12 @@
 import android.os.IBinder;
 import android.util.Log;
 
-import androidx.car.drawer.CarDrawerActivity;
 import androidx.car.drawer.CarDrawerAdapter;
 import androidx.car.drawer.DrawerItemViewHolder;
 import androidx.fragment.app.Fragment;
 
+import com.android.car.apps.common.DrawerActivity;
+
 import com.google.android.car.kitchensink.activityview.ActivityViewTestFragment;
 import com.google.android.car.kitchensink.alertdialog.AlertDialogTestFragment;
 import com.google.android.car.kitchensink.assistant.CarAssistantFragment;
@@ -61,11 +62,13 @@
 import com.google.android.car.kitchensink.touch.TouchTestFragment;
 import com.google.android.car.kitchensink.vhal.VehicleHalFragment;
 import com.google.android.car.kitchensink.volume.VolumeTestFragment;
+import com.google.android.car.kitchensink.weblinks.WebLinksTestFragment;
 
 import java.util.ArrayList;
 import java.util.List;
 
-public class KitchenSinkActivity extends CarDrawerActivity {
+
+public class KitchenSinkActivity extends DrawerActivity {
     private static final String TAG = "KitchenSinkActivity";
 
     private interface ClickHandler {
@@ -170,6 +173,7 @@
             });
             add("activity view", ActivityViewTestFragment.class);
             add("connectivity", ConnectivityFragment.class);
+            add("web links", WebLinksTestFragment.class);
             add("quit", KitchenSinkActivity.this::finish);
         }
 
@@ -207,14 +211,14 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setToolbarElevation(0f);
-        setMainContent(R.layout.kitchen_content);
-        getDrawerController().setRootAdapter(new DrawerAdapter());
+        setContentView(R.layout.kitchen_content);
+
         // Connection to Car Service does not work for non-automotive yet.
         if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
             initCarApi();
         }
         Log.i(TAG, "onCreate");
+        getDrawerController().setRootAdapter(new DrawerAdapter());
     }
 
     private void initCarApi() {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioPlayer.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioPlayer.java
index 74345aa..c13b0be 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioPlayer.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioPlayer.java
@@ -50,27 +50,37 @@
 
         @Override
         public void onAudioFocusChange(int focusChange) {
-            Log.i(TAG, "audio focus change " + focusChange);
             if (mPlayer == null) {
+                Log.e(TAG, "mPlayer is null");
                 return;
             }
             if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+                Log.i(TAG, "Audio focus change AUDIOFOCUS_GAIN for usage " + mAttrib.getUsage());
                 mPlayer.setVolume(1.0f, 1.0f);
                 if (mRepeat && isPlaying()) {
-                    doResume();
+                    // Resume
+                    Log.i(TAG, "resuming player");
+                    mPlayer.start();
                 }
             } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
-                if (isPlaying()) {
-                    // Duck to 20% volume (which matches system ducking as of this date)
-                    mPlayer.setVolume(0.2f, 0.2f);
-                }
-            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT && mRepeat) {
-                if (isPlaying()) {
-                    doPause();
+                // While we used to setVolume on the player to 20%, we don't do this anymore
+                // because we expect the car's audio hal do handle ducking as it sees fit.
+                Log.i(TAG, "Audio focus change AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> do nothing");
+            } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
+                Log.i(TAG, "Audio focus change AUDIOFOCUS_LOSS_TRANSIENT for usage "
+                        + mAttrib.getUsage());
+                if (mRepeat && isPlaying()) {
+                    Log.i(TAG, "pausing repeating player");
+                    mPlayer.pause();
+                } else {
+                    Log.i(TAG, "stopping one shot player");
+                    stop();
                 }
             } else {
+                Log.e(TAG, "Unrecognized audio focus change " + focusChange);
                 if (isPlaying()) {
-                    doStop();
+                    Log.i(TAG, "stopping player");
+                    stop();
                 }
             }
         }
@@ -102,13 +112,18 @@
         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
         int ret = AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
         if (mHandleFocus) {
+            // NOTE:  We are CONSCIOUSLY asking for focus again even if already playing in order
+            // exercise the framework's focus logic when faced with a (sloppy) application which
+            // might do this.
+            Log.i(TAG, "Asking for focus for usage " + mAttrib.getUsage());
             ret = mAudioManager.requestAudioFocus(mFocusListener, mAttrib,
                     focusRequest, 0);
         }
         if (ret == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+            Log.i(TAG, "MediaPlayer got focus for usage " + mAttrib.getUsage());
             doStart();
         } else {
-            Log.i(TAG, "no focus");
+            Log.i(TAG, "MediaPlayer denied focus for usage " + mAttrib.getUsage());
         }
     }
 
@@ -144,10 +159,10 @@
         mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
             @Override
             public void onCompletion(MediaPlayer mp) {
+                Log.i(TAG, "AudioPlayer onCompletion");
                 mPlaying.set(false);
                 if (!mRepeat && mHandleFocus) {
                     mPlayer.stop();
-                    mPlayer.release();
                     mPlayer = null;
                     mAudioManager.abandonAudioFocus(mFocusListener);
                     if (mListener != null) {
@@ -163,7 +178,7 @@
             AssetFileDescriptor afd =
                     mContext.getResources().openRawResourceFd(mResourceId);
             if (afd == null) {
-                throw new RuntimeException("no res");
+                throw new RuntimeException("resource not found");
             }
             mPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
                     afd.getLength());
@@ -176,35 +191,18 @@
     }
 
     public void stop() {
-        doStop();
-        if (mHandleFocus) {
-            mAudioManager.abandonAudioFocus(mFocusListener);
-        }
-    }
-
-    public void release() {
-        if (isPlaying()) {
-            stop();
-        }
-    }
-
-    private void doStop() {
         if (!mPlaying.getAndSet(false)) {
             Log.i(TAG, "already stopped");
             return;
         }
-        Log.i(TAG, "doStop audio");
+        Log.i(TAG, "stop");
+
         mPlayer.stop();
-        mPlayer.release();
         mPlayer = null;
-    }
 
-    private void doPause() {
-        mPlayer.pause();
-    }
-
-    private void doResume() {
-        mPlayer.start();
+        if (mHandleFocus) {
+            mAudioManager.abandonAudioFocus(mFocusListener);
+        }
     }
 
     public boolean isPlaying() {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
index 6325512..54c77a0 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
@@ -132,7 +132,7 @@
                         .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
                         .build();
                 mVrAudioAttrib = new AudioAttributes.Builder()
-                        .setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+                        .setUsage(AudioAttributes.USAGE_ASSISTANT)
                         .build();
                 mRadioAudioAttrib = new AudioAttributes.Builder()
                         .setUsage(AudioAttributes.USAGE_MEDIA)
@@ -147,7 +147,6 @@
                         mMusicAudioAttrib);
                 mNavGuidancePlayer = new AudioPlayer(mContext, R.raw.turnright,
                         mNavAudioAttrib);
-                // no Usage for voice command yet.
                 mVrPlayer = new AudioPlayer(mContext, R.raw.one2six,
                         mVrAudioAttrib);
                 mSystemPlayer = new AudioPlayer(mContext, R.raw.ring_classic_01,
@@ -197,6 +196,7 @@
         view.findViewById(R.id.button_wav_play_stop).setOnClickListener(v -> mWavPlayer.stop());
         view.findViewById(R.id.button_nav_play_once).setOnClickListener(v -> {
             if (mAppFocusManager == null) {
+                Log.e(TAG, "mAppFocusManager is null");
                 return;
             }
             if (DBG) {
@@ -217,6 +217,7 @@
         });
         view.findViewById(R.id.button_vr_play_once).setOnClickListener(v -> {
             if (mAppFocusManager == null) {
+                Log.e(TAG, "mAppFocusManager is null");
                 return;
             }
             if (DBG) {
@@ -300,6 +301,7 @@
 
     private void handleNavStart() {
         if (mAppFocusManager == null) {
+            Log.e(TAG, "mAppFocusManager is null");
             return;
         }
         if (DBG) {
@@ -317,6 +319,7 @@
 
     private void handleNavEnd() {
         if (mAppFocusManager == null) {
+            Log.e(TAG, "mAppFocusManager is null");
             return;
         }
         if (DBG) {
@@ -329,6 +332,7 @@
 
     private void handleVrStart() {
         if (mAppFocusManager == null) {
+            Log.e(TAG, "mAppFocusManager is null");
             return;
         }
         if (DBG) {
@@ -346,6 +350,7 @@
 
     private void handleVrEnd() {
         if (mAppFocusManager == null) {
+            Log.e(TAG, "mAppFocusManager is null");
             return;
         }
         if (DBG) {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java
index 3c04bcb..29c1d4c 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/ConnectivityFragment.java
@@ -18,8 +18,10 @@
 
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
+import android.graphics.Color;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
@@ -27,49 +29,396 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
 import android.widget.ListView;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import androidx.fragment.app.Fragment;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 
 import com.google.android.car.kitchensink.R;
 
-import java.util.ArrayList;
+import java.net.NetworkInterface;
+import java.net.SocketException;
 
 @SuppressLint("SetTextI18n")
 public class ConnectivityFragment extends Fragment {
     private static final String TAG = ConnectivityFragment.class.getSimpleName();
-
     private final Handler mHandler = new Handler();
-    private final ArrayList<String> mNetworks = new ArrayList<>();
 
     private ConnectivityManager mConnectivityManager;
-    private ArrayAdapter<String> mNetworksAdapter;
 
-    private final NetworkCallback mNetworkCallback = new NetworkCallback() {
-        @Override
-        public void onAvailable(Network network) {
-            showToast("onAvailable, netId: " + network);
-            refreshNetworks();
+    // Sort out current Network objects (NetId -> Network)
+    private SparseArray<Network> mNetworks = new SparseArray<Network>();
+
+    /**
+     * Create our own network callback object to use with NetworkRequests. Contains a reference to
+     * a Network so we can be sure to only surface updates on the network we want to see them on.
+     * We have to do this because there isn't a way to say "give me this SPECIFIC network." There's
+     * only "give me A network with these capabilities/transports."
+     */
+    public class NetworkByIdCallback extends NetworkCallback {
+        private final Network mNetwork;
+
+        NetworkByIdCallback(Network n) {
+            mNetwork = n;
         }
 
         @Override
-        public void onLost(Network network) {
-            showToast("onLost, netId: " + network);
-            refreshNetworks();
+        public void onAvailable(Network n) {
+            if (mNetwork.equals(n)) {
+                showToast("onAvailable(), netId: " + n);
+            }
         }
-    };
+
+        @Override
+        public void onLosing(Network n, int maxMsToLive) {
+            if (mNetwork.equals(n)) {
+                showToast("onLosing(), netId: " + n);
+            }
+        }
+
+        @Override
+        public void onLost(Network n) {
+            if (mNetwork.equals(n)) {
+                showToast("onLost(), netId: " + n);
+            }
+        }
+    }
+
+    // Map of NetId -> NetworkByIdCallback Objects -- Used to release requested networks
+    SparseArray<NetworkByIdCallback> mNetworkCallbacks = new SparseArray<NetworkByIdCallback>();
+
+    /**
+     * Implement a swipe-to-refresh list of available networks. NetworkListAdapter takes an array
+     * of NetworkItems that it cascades to the view. SwipeRefreshLayout wraps the adapter.
+     */
+    public static class NetworkItem {
+        public int mNetId;
+        public String mType;
+        public String mState;
+        public String mConnected;
+        public String mAvailable;
+        public String mRoaming;
+        public String mInterfaceName;
+        public String mHwAddress;
+        public String mIpAddresses;
+        public String mDnsAddresses;
+        public String mDomains;
+        public String mRoutes;
+        public String mTransports;
+        public String mCapabilities;
+        public String mBandwidth;
+        public boolean mDefault;
+        public boolean mRequested;
+    }
+
+    private NetworkItem[] mNetworkItems = new NetworkItem[0];
+    private NetworkListAdapter mNetworksAdapter;
+    private SwipeRefreshLayout mNetworkListRefresher;
+
+    /**
+     * Builds a NetworkRequest fit to a given network in the hope that we just get updates on that
+     * one network. This is the best way to get single network updates right now, as the request
+     * system works only on transport and capability requirements. There aaaare "network
+     * specifiers" but those only work based on the transport (i.e "eth0" would ask type ETHERNET
+     * for the correct interface where as "GoogleGuest" might ask type WIFI for the Network on SSID
+     * "GoogleGuest"). Ends up being paired with the custom callback above to only surface events
+     * for the specific network in question as well.
+     */
+    private NetworkRequest getRequestForNetwork(Network n) {
+        NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities(n);
+
+        NetworkRequest.Builder b = new NetworkRequest.Builder();
+        b.clearCapabilities();
+
+        for (int transportType : nc.getTransportTypes()) {
+            b.addTransportType(transportType);
+        }
+
+        for (int capability : nc.getCapabilities()) {
+            // Not all capabilities are requestable. According to source, all mutable capabilities
+            // except trusted are not requestable. Trying to request them results in an error being
+            // thrown
+            if (isRequestableCapability(capability)) {
+                b.addCapability(capability);
+            }
+        }
+
+        return b.build();
+    }
+
+    private boolean isRequestableCapability(int c) {
+        if (c == NetworkCapabilities.NET_CAPABILITY_VALIDATED
+                || c == NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+                || c == NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+                || c == NetworkCapabilities.NET_CAPABILITY_FOREGROUND
+                || c == NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
+                || c == NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) {
+            return false;
+        }
+        return true;
+    }
+
+    public void requestNetworkById(int netId) {
+        if (mNetworkCallbacks.get(netId) != null) {
+            return;
+        }
+
+        Network network = mNetworks.get(netId);
+        if (network == null) {
+            return;
+        }
+
+        NetworkRequest request = getRequestForNetwork(network);
+        NetworkByIdCallback cb = new NetworkByIdCallback(network);
+        mNetworkCallbacks.put(netId, cb);
+        mConnectivityManager.requestNetwork(request, cb);
+        showToast("Requesting Network " + netId);
+    }
+
+    public void releaseNetworkById(int netId) {
+        NetworkByIdCallback cb = mNetworkCallbacks.get(netId);
+        if (cb != null) {
+            mConnectivityManager.unregisterNetworkCallback(cb);
+            mNetworkCallbacks.remove(netId);
+            showToast("Released Network " + netId);
+        }
+    }
+
+    public void releaseAllNetworks() {
+        for (NetworkItem n : mNetworkItems) {
+            releaseNetworkById(n.mNetId);
+        }
+    }
+
+    public void bindToNetwork(int netId) {
+        Network network = mNetworks.get(netId);
+        if (network == null) {
+            return;
+        }
+
+        Network def = mConnectivityManager.getBoundNetworkForProcess();
+        if (def != null && def.netId != netId) {
+            clearBoundNetwork();
+        }
+        mConnectivityManager.bindProcessToNetwork(network);
+        showToast("Set process default network " + netId);
+    }
+
+    public void clearBoundNetwork() {
+        mConnectivityManager.bindProcessToNetwork(null);
+        showToast("Clear process default network");
+    }
+
+    public void reportNetworkbyId(int netId) {
+        Network network = mNetworks.get(netId);
+        if (network == null) {
+            return;
+        }
+        mConnectivityManager.reportNetworkConnectivity(network, false);
+        showToast("Reporting Network " + netId);
+    }
+
+    /**
+    * Maps of NET_CAPABILITY_* and TRANSPORT_* to string representations. A network having these
+    * capabilities will have the following strings print on their list entry.
+    */
+    private static final SparseArray<String> sTransportNames = new SparseArray<String>();
+    private static final SparseArray<String> sCapabilityNames = new SparseArray<String>();
+    static {
+        sTransportNames.put(NetworkCapabilities.TRANSPORT_LOWPAN, "[LOWPAN]");
+        sTransportNames.put(NetworkCapabilities.TRANSPORT_WIFI_AWARE, "[WIFI-AWARE]");
+        sTransportNames.put(NetworkCapabilities.TRANSPORT_VPN, "[VPN]");
+        sTransportNames.put(NetworkCapabilities.TRANSPORT_ETHERNET, "[ETHERNET]");
+        sTransportNames.put(NetworkCapabilities.TRANSPORT_BLUETOOTH, "[BLUETOOTH]");
+        sTransportNames.put(NetworkCapabilities.TRANSPORT_WIFI, "[WIFI]");
+        sTransportNames.put(NetworkCapabilities.TRANSPORT_CELLULAR, "[CELLULAR]");
+
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL, "[CAPTIVE PORTAL]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_CBS, "[CBS]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_DUN, "[DUN]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_EIMS, "[EIMS]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_FOREGROUND, "[FOREGROUND]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_FOTA, "[FOTA]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_IA, "[IA]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_IMS, "[IMS]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_INTERNET, "[INTERNET]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_MMS, "[MMS]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED, "[NOT CONGESTED]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, "[NOT METERED]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED, "[NOT RESTRICTED]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, "[NOT ROAMING]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED, "[NOT SUSPENDED]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_NOT_VPN, "[NOT VPN]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_RCS, "[RCS]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_SUPL, "[SUPL]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_TRUSTED, "[TRUSTED]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_VALIDATED, "[VALIDATED]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P, "[WIFI P2P]");
+        sCapabilityNames.put(NetworkCapabilities.NET_CAPABILITY_XCAP, "[XCAP]");
+    }
+
+    /**
+     * Builds a string out of the possible transports that can be applied to a
+     * NetworkCapabilities object.
+     */
+    private String getTransportString(NetworkCapabilities nCaps) {
+        String transports = "";
+        for (int transport : nCaps.getTransportTypes()) {
+            transports += sTransportNames.get(transport, "");
+        }
+        return transports;
+    }
+
+    /**
+     * Builds a string out of the possible capabilities that can be applied to
+     * a NetworkCapabilities object.
+    */
+    private String getCapabilitiesString(NetworkCapabilities nCaps) {
+        String caps = "";
+        for (int capability : nCaps.getCapabilities()) {
+            caps += sCapabilityNames.get(capability, "");
+        }
+        return caps;
+    }
+
+    // Gets the string representation of a MAC address from a given NetworkInterface object
+    private String getMacAddress(NetworkInterface ni) {
+        if (ni == null) {
+            return "??:??:??:??:??:??";
+        }
+
+        byte[] mac = null;
+        try {
+            mac = ni.getHardwareAddress();
+        } catch (SocketException exception) {
+            Log.e(TAG, "SocketException -- Failed to get interface MAC address");
+            return "??:??:??:??:??:??";
+        }
+
+        if (mac == null) {
+            return "??:??:??:??:??:??";
+        }
+
+        StringBuilder sb = new StringBuilder(18);
+        for (byte b : mac) {
+            if (sb.length() > 0) {
+                sb.append(':');
+            }
+            sb.append(String.format("%02x", b));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Builds a NetworkItem object from a given Network object, aggregating info across Network,
+     * NetworkCapabilities, NetworkInfo, NetworkInterface, and LinkProperties objects and pass it
+     * all as a string for the UI to use
+     */
+    private NetworkItem getNetworkItem(Network n) {
+
+        // Get default network to assign the button text correctly
+        // NOTE: activeNetwork != ProcessDefault when you set one, active is tracking the default
+        //       request regardless of your process's default
+        // Network defNetwork = mConnectivityManager.getActiveNetwork();
+        Network defNetwork = mConnectivityManager.getBoundNetworkForProcess();
+
+        // Used to get network state
+        NetworkInfo nInfo = mConnectivityManager.getNetworkInfo(n);
+
+        // Used to get transport type(s), capabilities
+        NetworkCapabilities nCaps = mConnectivityManager.getNetworkCapabilities(n);
+
+        // Properties of the actual physical link
+        LinkProperties nLink = mConnectivityManager.getLinkProperties(n);
+
+        // Object representing the actual interface
+        NetworkInterface nIface = null;
+        try {
+            nIface = NetworkInterface.getByName(nLink.getInterfaceName());
+        } catch (SocketException exception) {
+            Log.e(TAG, "SocketException -- Failed to get interface info");
+        }
+
+        // Pack NetworkItem with all values
+        NetworkItem ni = new NetworkItem();
+
+        // Row key
+        ni.mNetId = n.netId;
+
+        // LinkProperties/NetworkInterface
+        ni.mInterfaceName = "Interface: " + nLink.getInterfaceName()
+                            + (nIface != null ? " (" + nIface.getName() + ")" : " ()");
+        ni.mHwAddress = "HwAddress: " + getMacAddress(nIface);
+        ni.mIpAddresses = "IP Addresses: " + nLink.getLinkAddresses().toString();
+        ni.mDnsAddresses = "DNS: " + nLink.getDnsServers().toString();
+        ni.mDomains = "Domains: " + nLink.getDomains();
+        ni.mRoutes = "Routes: " + nLink.getRoutes().toString();
+
+        // NetworkInfo
+        ni.mType = "Type: " + nInfo.getTypeName() + " (" + nInfo.getSubtypeName() + ")";
+        ni.mState = "State: " + nInfo.getState().name() + "/" + nInfo.getDetailedState().name();
+        ni.mConnected = "Connected: " + (nInfo.isConnected() ? "Connected" : "Disconnected");
+        ni.mAvailable = "Available: " + (nInfo.isAvailable() ? "Yes" : "No");
+        ni.mRoaming = "Roaming: " + (nInfo.isRoaming() ? "Yes" : "No");
+
+        // NetworkCapabilities
+        ni.mTransports = "Transports: " + getTransportString(nCaps);
+        ni.mCapabilities = "Capabilities: " + getCapabilitiesString(nCaps);
+        ni.mBandwidth = "Bandwidth (Down/Up): " + nCaps.getLinkDownstreamBandwidthKbps()
+                        + " Kbps/" + nCaps.getLinkUpstreamBandwidthKbps() + " Kbps";
+
+        // Other inferred values
+        ni.mDefault = sameNetworkId(n, defNetwork);
+        ni.mRequested = (mNetworkCallbacks.get(n.netId) != null);
+
+        return ni;
+    }
+
+    // Refresh the networks content and prompt the user that we did it
+    private void refreshNetworksAndPrompt() {
+        refreshNetworks();
+        showToast("Refreshed Networks (" + mNetworkItems.length + ")");
+    }
+
+    /**
+     * Gets the current set of networks from the connectivity manager and 1) stores the network
+     * objects 2) builds NetworkItem objects for the view to render and 3) If a network we were
+     * tracking disappears then it kills its callback.
+     */
+    private void refreshNetworks() {
+        Log.i(TAG, "refreshNetworks()");
+        Network[] networks = mConnectivityManager.getAllNetworks();
+        mNetworkItems = new NetworkItem[networks.length];
+        mNetworks.clear();
+
+        // Add each network to the network info set, turning each field to a string
+        for (int i = 0; i < networks.length; i++) {
+            mNetworkItems[i] = getNetworkItem(networks[i]);
+            mNetworks.put(networks[i].netId, networks[i]);
+        }
+
+        // Check for callbacks that belong to networks that don't exist anymore
+        for (int i = 0; i < mNetworkCallbacks.size(); i++) {
+            int key = mNetworkCallbacks.keyAt(i);
+            if (mNetworks.get(key) == null) {
+                mNetworkCallbacks.remove(key);
+            }
+        }
+
+        // Update the view
+        mNetworksAdapter.refreshNetworks(mNetworkItems);
+    }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
         mConnectivityManager = getActivity().getSystemService(ConnectivityManager.class);
-
         mConnectivityManager.addDefaultNetworkActiveListener(() -> refreshNetworks());
     }
 
@@ -79,77 +428,41 @@
             @Nullable Bundle savedInstanceState) {
         View view = inflater.inflate(R.layout.connectivity_fragment, container, false);
 
+        // Create the ListView of all networks
         ListView networksView = view.findViewById(R.id.networks);
-        mNetworksAdapter = new ArrayAdapter<>(getActivity(), R.layout.list_item, mNetworks);
+        mNetworksAdapter = new NetworkListAdapter(getContext(), mNetworkItems, this);
         networksView.setAdapter(mNetworksAdapter);
 
-        setClickAction(view, R.id.networksRefresh, this::refreshNetworks);
-        setClickAction(view, R.id.networkRequestOemPaid, this::requestOemPaid);
-        setClickAction(view, R.id.networkRequestEth1, this::requestEth1);
-        setClickAction(view, R.id.networkReleaseNetwork, this::releaseNetworkRequest);
+        // Find all networks ListView refresher and set the refresh callback
+        mNetworkListRefresher = (SwipeRefreshLayout) view.findViewById(R.id.refreshNetworksList);
+        mNetworkListRefresher.setOnRefreshListener(() -> {
+            refreshNetworksAndPrompt();
+            mNetworkListRefresher.setRefreshing(false);
+        });
 
         return view;
     }
 
-    private void releaseNetworkRequest() {
-        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
-        showToast("Release request sent");
-    }
-
-    private void requestEth1() {
-        NetworkRequest request = new NetworkRequest.Builder()
-                .clearCapabilities()
-                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
-                .setNetworkSpecifier("eth1")
-                .build();
-        mConnectivityManager.requestNetwork(request, mNetworkCallback, mHandler);
-    }
-
-    private void requestOemPaid() {
-        NetworkRequest request = new NetworkRequest.Builder()
-                .addCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID)
-                .build();
-
-        mConnectivityManager.requestNetwork(request, mNetworkCallback, mHandler);
-    }
-
     @Override
     public void onResume() {
         super.onResume();
         refreshNetworks();
     }
 
-    private void setClickAction(View view, int id, Runnable action) {
-        view.findViewById(id).setOnClickListener(v -> action.run());
+    @Override
+    public void onPause() {
+        super.onPause();
+        releaseAllNetworks();
     }
 
-    private void refreshNetworks() {
-        mNetworks.clear();
-
-        for (Network network : mConnectivityManager.getAllNetworks()) {
-            boolean isDefault = sameNetworkId(network, mConnectivityManager.getActiveNetwork());
-            NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities(network);
-            boolean isOemPaid = nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID);
-            boolean isInternet = nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-
-            NetworkInfo networkInfo = mConnectivityManager.getNetworkInfo(network);
-
-            mNetworks.add("netId: " + network.netId
-                    + (isInternet ? " [INTERNET]" : "")
-                    + (isDefault ? " [DEFAULT]" : "")
-                    + (isOemPaid ? " [OEM-paid]" : "") + nc + " " + networkInfo);
-        }
-
-        mNetworksAdapter.notifyDataSetChanged();
-    }
-
-    private void showToast(String text) {
-        Log.d(TAG, "showToast: " + text);
-        Toast.makeText(getContext(), text, Toast.LENGTH_LONG).show();
+    public void showToast(String text) {
+        Toast toast = Toast.makeText(getContext(), text, Toast.LENGTH_SHORT);
+        TextView v = (TextView) toast.getView().findViewById(android.R.id.message);
+        v.setTextColor(Color.WHITE);
+        toast.show();
     }
 
     private static boolean sameNetworkId(Network net1, Network net2) {
         return net1 != null && net2 != null && net1.netId == net2.netId;
-
     }
 }
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/NetworkListAdapter.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/NetworkListAdapter.java
new file mode 100644
index 0000000..b726654
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/connectivity/NetworkListAdapter.java
@@ -0,0 +1,208 @@
+/*
+ * 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.google.android.car.kitchensink.connectivity;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.google.android.car.kitchensink.R;
+import com.google.android.car.kitchensink.connectivity.ConnectivityFragment.NetworkItem;
+
+public class NetworkListAdapter extends ArrayAdapter<NetworkItem> {
+    private static final String TAG = NetworkListAdapter.class.getSimpleName();
+
+    private Context mContext;
+    private NetworkItem[] mNetworkList; // keep list of objects
+    private ConnectivityFragment mFragment; // for calling things on button press
+
+    public NetworkListAdapter(Context context,  NetworkItem[] items,
+                              ConnectivityFragment fragment) {
+        super(context, R.layout.network_item, items);
+        mContext = context;
+        mFragment = fragment;
+        mNetworkList = items;
+
+        Log.i(TAG, "Created NetworkListAdaptor");
+    }
+
+    // Returns a list item view for each position
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        ViewHolder vh;
+        if (convertView == null) {
+            vh = new ViewHolder();
+            LayoutInflater inflater = LayoutInflater.from(mContext);
+            convertView = inflater.inflate(R.layout.network_item, parent, false);
+            vh.netActive = convertView.findViewById(R.id.network_active);
+            vh.netId = convertView.findViewById(R.id.network_id);
+            vh.netType = convertView.findViewById(R.id.network_type);
+            vh.netState = convertView.findViewById(R.id.network_state);
+            vh.connected = convertView.findViewById(R.id.network_connected);
+            vh.available = convertView.findViewById(R.id.network_available);
+            vh.roaming = convertView.findViewById(R.id.network_roaming);
+            vh.netIface = convertView.findViewById(R.id.network_iface);
+            vh.hwAddress = convertView.findViewById(R.id.hw_address);
+            vh.ipAddresses = convertView.findViewById(R.id.network_ip_addresses);
+            vh.dns = convertView.findViewById(R.id.network_dns);
+            vh.domains = convertView.findViewById(R.id.network_domains);
+            vh.routes = convertView.findViewById(R.id.network_routes);
+            vh.transports = convertView.findViewById(R.id.network_transports);
+            vh.capabilities = convertView.findViewById(R.id.network_capabilities);
+            vh.bandwidth = convertView.findViewById(R.id.network_bandwidth);
+            vh.requestButton = convertView.findViewById(R.id.network_request);
+            vh.defaultButton = convertView.findViewById(R.id.network_default);
+            vh.reportButton = convertView.findViewById(R.id.network_report);
+
+            convertView.setTag(vh);
+        } else {
+            vh = (ViewHolder) convertView.getTag();
+        }
+
+        // If there's data to fill for the given position in the list
+        if (position < getCount()) {
+            vh.netId.setText("" + mNetworkList[position].mNetId);
+            vh.netType.setText(mNetworkList[position].mType);
+            vh.netState.setText(mNetworkList[position].mState);
+            vh.connected.setText(mNetworkList[position].mConnected);
+            vh.available.setText(mNetworkList[position].mAvailable);
+            vh.roaming.setText(mNetworkList[position].mRoaming);
+            vh.netIface.setText(mNetworkList[position].mInterfaceName);
+            vh.hwAddress.setText(mNetworkList[position].mHwAddress);
+            vh.ipAddresses.setText(mNetworkList[position].mIpAddresses);
+            vh.dns.setText(mNetworkList[position].mDnsAddresses);
+            vh.domains.setText(mNetworkList[position].mDomains);
+            vh.routes.setText(mNetworkList[position].mRoutes);
+            vh.transports.setText(mNetworkList[position].mTransports);
+            vh.capabilities.setText(mNetworkList[position].mCapabilities);
+            vh.bandwidth.setText(mNetworkList[position].mBandwidth);
+
+            // Active request indicator
+            vh.netActive.setBackgroundColor(mNetworkList[position].mRequested
+                    ? Color.parseColor("#5fdd6e")
+                    : Color.parseColor("#ff3d3d"));
+
+            // Request to track button
+            setToggleButton(position, vh.requestButton, mNetworkList[position].mRequested,
+                    "Release", "Request", this::onRequestClicked);
+
+            // Process default button
+            setToggleButton(position, vh.defaultButton, mNetworkList[position].mDefault,
+                    "Remove Default", "Set Default", this::onDefaultClicked);
+
+            // Report network button
+            setPositionTaggedCallback(position, vh.reportButton, this::onReportClicked);
+        }
+
+        // Alternate table row background color to make it easier to view
+        convertView.setBackgroundColor(((position % 2) != 0)
+                ? Color.parseColor("#2A2E2D")
+                : Color.parseColor("#1E1E1E"));
+
+        return convertView;
+    }
+
+    // Tags a button with its element position and assigned it's callback. The callback can then
+    // get the tag and use it as a position to know which data is associated with it
+    private void setPositionTaggedCallback(int position, Button button, View.OnClickListener l) {
+        button.setTag(position);
+        button.setOnClickListener(l);
+    }
+
+    private void setToggleButton(int position, Button button, boolean on, String ifOn, String ifOff,
+            View.OnClickListener l) {
+        // Manage button text based on status
+        if (on) {
+            button.setText(ifOn);
+        } else {
+            button.setText(ifOff);
+        }
+        setPositionTaggedCallback(position, button, l);
+    }
+
+    private void onRequestClicked(View view) {
+        int position = (int) view.getTag();
+        if (mNetworkList[position].mRequested) {
+            mFragment.releaseNetworkById(mNetworkList[position].mNetId);
+            mNetworkList[position].mRequested = false;
+        } else {
+            mFragment.requestNetworkById(mNetworkList[position].mNetId);
+            mNetworkList[position].mRequested = true;
+        }
+        notifyDataSetChanged();
+    }
+
+    private void onDefaultClicked(View view) {
+        int position = (int) view.getTag();
+        if (mNetworkList[position].mDefault) {
+            mFragment.clearBoundNetwork();
+            mNetworkList[position].mDefault = false;
+        } else {
+            for (int i = 0; i < mNetworkList.length; i++) {
+                if (i != position) {
+                    mNetworkList[i].mDefault = false;
+                }
+            }
+            mFragment.bindToNetwork(mNetworkList[position].mNetId);
+            mNetworkList[position].mDefault = true;
+        }
+        notifyDataSetChanged();
+    }
+
+    private void onReportClicked(View view) {
+        int position = (int) view.getTag();
+        mFragment.reportNetworkbyId(mNetworkList[position].mNetId);
+    }
+
+    public void refreshNetworks(NetworkItem[] networksIn) {
+        mNetworkList = networksIn;
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public int getCount() {
+        return mNetworkList.length;
+    }
+
+    static class ViewHolder {
+        public View netActive;
+        public TextView netId;
+        public TextView netType;
+        public TextView netState;
+        public TextView connected;
+        public TextView available;
+        public TextView roaming;
+        public TextView netIface;
+        public TextView hwAddress;
+        public TextView ipAddresses;
+        public TextView dns;
+        public TextView domains;
+        public TextView routes;
+        public TextView transports;
+        public TextView capabilities;
+        public TextView bandwidth;
+        public Button requestButton;
+        public Button defaultButton;
+        public Button reportButton;
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/displayinfo/DisplayInfoFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/displayinfo/DisplayInfoFragment.java
index ac0bdae..e199821 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/displayinfo/DisplayInfoFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/displayinfo/DisplayInfoFragment.java
@@ -32,7 +32,7 @@
 import com.google.android.car.kitchensink.R;
 
 /**
- * Shows alert dialogs
+ * Displays info about the display this is run on.
  */
 public class DisplayInfoFragment extends Fragment {
 
@@ -95,11 +95,18 @@
     }
 
     private void addDimenText(String dimenName) {
-        addTextView(dimenName + " : " + convertPixelsToDp(
-                getResources().getDimensionPixelSize(
-                        getResources().getIdentifier(
-                                dimenName, "dimen", getContext().getPackageName())),
-                getContext()));
+        String value;
+        try {
+            float dimen = convertPixelsToDp(
+                    getResources().getDimensionPixelSize(
+                            getResources().getIdentifier(
+                                    dimenName, "dimen", getContext().getPackageName())),
+                    getContext());
+            value = Float.toString(dimen);
+        } catch (Resources.NotFoundException e) {
+            value = "Resource Not Found";
+        }
+        addTextView(dimenName + " : " + value);
     }
 
     private void addTextView(String text) {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
index 5128816..34ea1cf 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/input/InputTestFragment.java
@@ -30,7 +30,6 @@
 import android.hardware.automotive.vehicle.V2_0.VehicleArea;
 import android.hardware.automotive.vehicle.V2_0.VehicleDisplay;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
-import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyGroup;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyStatus;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyType;
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
index 7a26f93..d7230fc 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
@@ -45,6 +45,8 @@
         Button importanceDefaultButton = view.findViewById(R.id.importance_default_button);
         Button ongoingButton = view.findViewById(R.id.ongoing_button);
         Button messageButton = view.findViewById(R.id.category_message_button);
+        Button emerg = view.findViewById(R.id.category_car_emerg_button);
+        Button warn = view.findViewById(R.id.category_car_warning_button);
 
         NotificationManager manager =
                 (NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE);
@@ -184,6 +186,28 @@
             manager.notify(3, notification);
         });
 
+        emerg.setOnClickListener(v -> {
+
+            Notification notification = new Notification.Builder(getActivity(), CHANNEL_ID_1)
+                    .setContentTitle("OMG")
+                    .setContentText("This is of top importance")
+                    .setCategory(Notification.CATEGORY_CAR_EMERGENCY)
+                    .setSmallIcon(R.drawable.car_ic_mode)
+                    .build();
+            manager.notify(10, notification);
+        });
+
+        warn.setOnClickListener(v -> {
+
+            Notification notification = new Notification.Builder(getActivity(), CHANNEL_ID_1)
+                    .setContentTitle("OMG -ish ")
+                    .setContentText("This is of less importance but still")
+                    .setCategory(Notification.CATEGORY_CAR_WARNING)
+                    .setSmallIcon(R.drawable.car_ic_mode)
+                    .build();
+            manager.notify(11, notification);
+        });
+
         return view;
     }
 }
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/LocationListeners.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/LocationListeners.java
index b99f5da..5d0c02e 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/LocationListeners.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/LocationListeners.java
@@ -71,9 +71,9 @@
             mSensorMgr.registerListener(mSensorListener, magneticFieldSensor,
                     SensorManager.SENSOR_DELAY_FASTEST);
 
-            mTextUpdateHandler.setAccelField("waiting to hear from SensorManager");
-            mTextUpdateHandler.setGyroField("waiting to hear from SensorManager");
-            mTextUpdateHandler.setMagField("waiting to hear from SensorManager");
+            mTextUpdateHandler.setAccelField("Accel waiting to hear from SensorManager");
+            mTextUpdateHandler.setGyroField("Gyro waiting to hear from SensorManager");
+            mTextUpdateHandler.setMagField("Mag waiting to hear from SensorManager");
         } else {
             mTextUpdateHandler.setAccelField("SensorManager not available");
             mTextUpdateHandler.setGyroField("SensorManager not available");
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java
index 77a6e1c..82a8cb2 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/sensor/SensorsTestFragment.java
@@ -43,7 +43,6 @@
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -343,11 +342,11 @@
         if (event == null) {
             return mNaString;
         }
-        return mDateFormat.format(new Date(event.timestamp / (1000L * 1000L)));
+        return Double.toString(event.timestamp / (1000L * 1000L * 1000L)) + " seconds";
     }
 
     private String getTimestampNow() {
-        return mDateFormat.format(new Date(System.nanoTime() / (1000L * 1000L)));
+        return Double.toString(System.nanoTime() / (1000L * 1000L * 1000L)) + " seconds";
     }
 
     private String getFuelLevel(CarSensorEvent event) {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/weblinks/WebLinksTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/weblinks/WebLinksTestFragment.java
new file mode 100644
index 0000000..2d2f864
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/weblinks/WebLinksTestFragment.java
@@ -0,0 +1,57 @@
+/*
+ * 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.google.android.car.kitchensink.weblinks;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.car.kitchensink.R;
+
+/**
+ * This fragment just has a few links to web pages.
+ */
+public class WebLinksTestFragment extends Fragment {
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View root = inflater.inflate(R.layout.weblinks_fragment, container, false);
+
+        LinearLayout buttons = root.findViewById(R.id.buttons);
+        for (int i = 0; i < buttons.getChildCount(); i++) {
+            buttons.getChildAt(i).setOnClickListener(this::onClick);
+        }
+
+        return root;
+    }
+
+    private void onClick(View view) {
+        String url = view.getTag().toString();
+
+        if (!url.startsWith("http")) {
+            url = "http://" + url;
+        }
+        startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
+    }
+}
diff --git a/tests/UxRestrictionsSample/AndroidManifest.xml b/tests/UxRestrictionsSample/AndroidManifest.xml
index 43a6362..bd28e0d 100644
--- a/tests/UxRestrictionsSample/AndroidManifest.xml
+++ b/tests/UxRestrictionsSample/AndroidManifest.xml
@@ -19,7 +19,7 @@
     <uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
 
     <application android:label="UxRestrictions Sample">
-        <activity android:name=".MainActivity" android:theme="@style/Theme.Car.NoActionBar">
+        <activity android:name=".MainActivity" android:theme="@style/AppTheme" android:launchMode="singleTask">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
                 <category android:name="android.intent.category.LAUNCHER"/>
@@ -27,7 +27,7 @@
             <meta-data android:name="distractionOptimized" android:value="true"/>
         </activity>
         <activity android:name=".SampleMessageActivity"
-                  android:theme="@style/Theme.Car.NoActionBar">
+                  android:theme="@style/AppTheme">
             <meta-data android:name="distractionOptimized" android:value="true"/>
         </activity>
     </application>
diff --git a/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml b/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml
index 20c6465..a778975 100644
--- a/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml
+++ b/tests/UxRestrictionsSample/res/layout/activity_sample_message.xml
@@ -13,29 +13,24 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<androidx.car.moderator.SpeedBumpView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent">
-        <Button
-            android:id="@+id/home_button"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/return_home"
-            android:textAllCaps="false"
-            android:textAppearance="?android:textAppearanceLarge"/>
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content">
-            <androidx.car.widget.PagedListView
-                android:id="@+id/paged_list_view"
-                android:layout_height="match_parent"
-                android:layout_width="wrap_content"
-                android:layout_weight="4"/>
-        </LinearLayout>
-    </LinearLayout>
-</androidx.car.moderator.SpeedBumpView>
+  <Button
+      android:id="@+id/home_button"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="@string/return_home"
+      android:textAllCaps="false"
+      android:textAppearance="?android:textAppearanceLarge"/>
+  <LinearLayout
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content">
+    <androidx.car.widget.PagedListView
+        android:id="@+id/paged_list_view"
+        android:layout_height="match_parent"
+        android:layout_width="wrap_content"
+        android:layout_weight="4"/>
+  </LinearLayout>
+</LinearLayout>
diff --git a/tests/UxRestrictionsSample/res/layout/main_activity.xml b/tests/UxRestrictionsSample/res/layout/main_activity.xml
index 695e93b..851497a 100644
--- a/tests/UxRestrictionsSample/res/layout/main_activity.xml
+++ b/tests/UxRestrictionsSample/res/layout/main_activity.xml
@@ -13,114 +13,137 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<androidx.car.moderator.SpeedBumpView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
-    <LinearLayout
-        android:orientation="horizontal"
+
+  <LinearLayout
+      android:layout_height="match_parent"
+      android:layout_width="0dp"
+      android:layout_weight="1"
+      android:orientation="vertical">
+
+    <TextView
+        android:text="@string/status_header"
+        android:layout_gravity="center"
+        android:padding="@dimen/section_padding"
+        android:textSize="@dimen/header_text_size"
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
+        android:textAppearance="?android:textAppearanceLarge"
+        android:layout_height="wrap_content"/>
 
-        <LinearLayout
-            android:layout_height="match_parent"
-            android:layout_width="0dp"
-            android:layout_weight="1"
-            android:orientation="vertical">
+    <TextView
+        android:id="@+id/driving_state"
+        android:text="@string/driving_state"
+        android:textSize="@dimen/info_text_size"
+        android:layout_gravity="center"
+        android:padding="@dimen/section_padding"
+        android:layout_width="match_parent"
+        android:textAppearance="?android:textAppearanceLarge"
+        android:layout_height="wrap_content"/>
 
-            <TextView
-                android:text="@string/status_header"
-                android:layout_gravity="center"
-                android:padding="@dimen/section_padding"
-                android:textSize="@dimen/header_text_size"
-                android:layout_width="match_parent"
-                android:textAppearance="?android:textAppearanceLarge"
-                android:layout_height="wrap_content" />
+    <TextView
+        android:id="@+id/do_status"
+        android:text="@string/is_do_reqd"
+        android:textSize="@dimen/info_text_size"
+        android:padding="@dimen/section_padding"
+        android:layout_width="match_parent"
+        android:textAppearance="?android:textAppearanceLarge"
+        android:layout_height="wrap_content"/>
 
-            <TextView
-                android:id="@+id/driving_state"
-                android:text="@string/driving_state"
-                android:textSize="@dimen/info_text_size"
-                android:layout_gravity="center"
-                android:padding="@dimen/section_padding"
-                android:layout_width="match_parent"
-                android:textAppearance="?android:textAppearanceLarge"
-                android:layout_height="wrap_content" />
+    <TextView
+        android:id="@+id/uxr_status"
+        android:text="@string/active_restrictions"
+        android:padding="@dimen/section_padding"
+        android:textSize="@dimen/info_text_size"
+        android:layout_width="match_parent"
+        android:textAppearance="?android:textAppearanceLarge"
+        android:layout_height="wrap_content"/>
 
-            <TextView
-                android:id="@+id/do_status"
-                android:text="@string/is_do_reqd"
-                android:textSize="@dimen/info_text_size"
-                android:padding="@dimen/section_padding"
-                android:layout_width="match_parent"
-                android:textAppearance="?android:textAppearanceLarge"
-                android:layout_height="wrap_content" />
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:padding="@dimen/section_padding"
+        android:layout_marginBottom="10dp"
+        android:background="@android:color/darker_gray"/>
 
-            <TextView
-                android:id="@+id/uxr_status"
-                android:text="@string/active_restrictions"
-                android:padding="@dimen/section_padding"
-                android:textSize="@dimen/info_text_size"
-                android:layout_width="match_parent"
-                android:textAppearance="?android:textAppearanceLarge"
-                android:layout_height="wrap_content" />
+    <TextView
+        android:text="@string/action_header"
+        android:padding="@dimen/section_padding"
+        android:textSize="@dimen/header_text_size"
+        android:layout_width="match_parent"
+        android:textAppearance="?android:textAppearanceLarge"
+        android:layout_height="wrap_content"/>
 
-            <View
-                android:layout_width="match_parent"
-                android:layout_height="1dp"
-                android:padding="@dimen/section_padding"
-                android:layout_marginBottom="10dp"
-                android:background="@android:color/darker_gray"/>
-
-            <TextView
-                android:text="@string/action_header"
-                android:padding="@dimen/section_padding"
-                android:textSize="@dimen/header_text_size"
-                android:layout_width="match_parent"
-                android:textAppearance="?android:textAppearanceLarge"
-                android:layout_height="wrap_content" />
-
-            <LinearLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-                <Button
-                    android:id="@+id/toggle_status"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:padding="@dimen/section_padding"
-                    android:text="@string/disable_uxr"
-                    android:textAllCaps="false"
-                    android:textSize="@dimen/info_text_size" />
-            </LinearLayout>
-
-            <View
-                android:layout_width="match_parent"
-                android:layout_height="1dp"
-                android:layout_marginTop="@dimen/section_padding"
-                android:layout_marginBottom="10dp"
-                android:background="@android:color/darker_gray"/>
-
-            <TextView
-                android:text="@string/sample_header"
-                android:padding="@dimen/section_padding"
-                android:textSize="@dimen/header_text_size"
-                android:layout_width="match_parent"
-                android:textAppearance="?android:textAppearanceLarge"
-                android:layout_height="wrap_content" />
-
-            <LinearLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-                <Button
-                    android:id="@+id/launch_message"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:text="@string/sample_msg_activity"
-                    android:textAllCaps="false"
-                    android:textSize="@dimen/info_text_size" />
-            </LinearLayout>
-        </LinearLayout>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+      <Button
+          android:id="@+id/toggle_status"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:padding="@dimen/section_padding"
+          android:text="@string/disable_uxr"
+          android:textAllCaps="false"
+          android:textSize="@dimen/info_text_size"/>
     </LinearLayout>
-</androidx.car.moderator.SpeedBumpView>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_marginTop="@dimen/section_padding"
+        android:layout_marginBottom="10dp"
+        android:background="@android:color/darker_gray"/>
+
+    <TextView
+        android:text="@string/sample_header"
+        android:padding="@dimen/section_padding"
+        android:textSize="@dimen/header_text_size"
+        android:layout_width="match_parent"
+        android:textAppearance="?android:textAppearanceLarge"
+        android:layout_height="wrap_content"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+      <Button
+          android:id="@+id/launch_message"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:text="@string/sample_msg_activity"
+          android:textAllCaps="false"
+          android:textSize="@dimen/info_text_size"/>
+    </LinearLayout>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_marginTop="@dimen/section_padding"
+        android:layout_marginBottom="10dp"
+        android:background="@android:color/darker_gray"/>
+
+    <TextView
+        android:text="@string/save_uxr_config_header"
+        android:padding="@dimen/section_padding"
+        android:textSize="@dimen/header_text_size"
+        android:layout_width="match_parent"
+        android:textAppearance="?android:textAppearanceLarge"
+        android:layout_height="wrap_content"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+      <Button
+          android:id="@+id/save_uxr_config"
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:text="@string/save_uxr_config"
+          android:textSize="@dimen/info_text_size"/>
+    </LinearLayout>
+  </LinearLayout>
+</LinearLayout>
+
 
 
diff --git a/tests/UxRestrictionsSample/res/values/strings.xml b/tests/UxRestrictionsSample/res/values/strings.xml
index 612e251..3b0b69b 100644
--- a/tests/UxRestrictionsSample/res/values/strings.xml
+++ b/tests/UxRestrictionsSample/res/values/strings.xml
@@ -25,4 +25,9 @@
     <string name="sample_header"><u>Sample Activities</u></string>
     <string name="sample_msg_activity">Sample Message Activity</string>
     <string name="return_home"><u>Return Home</u></string>
+    <string name="save_uxr_config_header"><u>Save UX Restrictions For Next Boot</u></string>
+    <string name="save_uxr_config">Save UX Restrictions</string>
+    <string name="set_uxr_config_dialog_title">Select restrictions for IDLING/MOVING</string>
+    <string name="set_uxr_config_dialog_negative_button">Cancel</string>
+    <string name="set_uxr_config_dialog_positive_button">Save UXR Config</string>
 </resources>
diff --git a/tests/UxRestrictionsSample/res/values/styles.xml b/tests/UxRestrictionsSample/res/values/styles.xml
new file mode 100644
index 0000000..05d0d03
--- /dev/null
+++ b/tests/UxRestrictionsSample/res/values/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+
+    <style name="AppTheme" parent="@style/Theme.Car.NoActionBar">
+        <item name="android:windowBackground">@android:color/black</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java b/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java
index 104e145..cd9c015 100644
--- a/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java
+++ b/tests/UxRestrictionsSample/src/com/google/android/car/uxr/sample/MainActivity.java
@@ -15,13 +15,19 @@
  */
 package com.google.android.car.uxr.sample;
 
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_IDLING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_MOVING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED;
+
 import android.app.Activity;
+import android.app.AlertDialog;
 import android.car.Car;
 import android.car.CarNotConnectedException;
 import android.car.content.pm.CarPackageManager;
 import android.car.drivingstate.CarDrivingStateEvent;
 import android.car.drivingstate.CarDrivingStateManager;
 import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
 import android.car.drivingstate.CarUxRestrictionsManager;
 import android.content.ComponentName;
 import android.content.Intent;
@@ -39,6 +45,21 @@
  */
 public class MainActivity extends Activity {
     public static final String TAG = "drivingstate";
+
+    // Order of elements is based on number of bits shifted in value of the constants.
+    private static final CharSequence[] UX_RESTRICTION_NAMES = new CharSequence[] {
+            "BASELINE",
+            "NO_DIALPAD",
+            "NO_FILTERING",
+            "LIMIT_STRING_LENGTH",
+            "NO_KEYBOARD",
+            "NO_VIDEO",
+            "LIMIT_CONTENT",
+            "NO_SETUP",
+            "NO_TEXT_MESSAGE",
+            "NO_VOICE_TRANSCRIPTION",
+    };
+
     private Car mCar;
     private CarDrivingStateManager mCarDrivingStateManager;
     private CarUxRestrictionsManager mCarUxRestrictionsManager;
@@ -48,6 +69,7 @@
     private TextView mUxrStatus;
     private Button mToggleButton;
     private Button mSampleMsgButton;
+    private Button mSaveUxrConfigButton;
 
     private boolean mEnableUxR;
 
@@ -74,7 +96,6 @@
                             mCarUxRestrictionsManager.registerListener(mUxRChangeListener);
                             updateUxRText(mCarUxRestrictionsManager.getCurrentCarUxRestrictions());
                         }
-
                     } catch (CarNotConnectedException e) {
                         Log.e(TAG, "Failed to get a connection", e);
                     }
@@ -96,7 +117,9 @@
                         : "No Distraction Optimization required");
 
         mUxrStatus.setText("Active Restrictions : 0x"
-                + Integer.toHexString(restrictions.getActiveRestrictions()));
+                + Integer.toHexString(restrictions.getActiveRestrictions())
+                + " - "
+                + Integer.toBinaryString(restrictions.getActiveRestrictions()));
 
         mDistractionOptStatus.requestLayout();
         mUxrStatus.requestLayout();
@@ -123,13 +146,13 @@
         }
         String displayText;
         switch (state.eventValue) {
-            case CarDrivingStateEvent.DRIVING_STATE_PARKED:
+            case DRIVING_STATE_PARKED:
                 displayText = "Parked";
                 break;
-            case CarDrivingStateEvent.DRIVING_STATE_IDLING:
+            case DRIVING_STATE_IDLING:
                 displayText = "Idling";
                 break;
-            case CarDrivingStateEvent.DRIVING_STATE_MOVING:
+            case DRIVING_STATE_MOVING:
                 displayText = "Moving";
                 break;
             default:
@@ -147,8 +170,9 @@
             this::updateDrivingStateText;
 
     @Override
-    public void onCreate(Bundle savedInstanceState) {
+    protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
         setContentView(R.layout.main_activity);
 
         mDrvStatus = findViewById(R.id.driving_state);
@@ -156,9 +180,10 @@
         mUxrStatus = findViewById(R.id.uxr_status);
         mToggleButton = findViewById(R.id.toggle_status);
 
-        mToggleButton.setOnClickListener(v -> {
-            updateToggleUxREnable();
-        });
+        mSaveUxrConfigButton = findViewById(R.id.save_uxr_config);
+        mSaveUxrConfigButton.setOnClickListener(v -> saveUxrConfig());
+
+        mToggleButton.setOnClickListener(v -> updateToggleUxREnable());
 
         mSampleMsgButton = findViewById(R.id.launch_message);
         mSampleMsgButton.setOnClickListener(this::launchSampleMsgActivity);
@@ -168,14 +193,49 @@
         mCar.connect();
     }
 
+    private void saveUxrConfig() {
+        // Pop up a dialog to build the IDLING restrictions.
+        boolean[] selected = new boolean[UX_RESTRICTION_NAMES.length];
+        new AlertDialog.Builder(this)
+                .setTitle(R.string.set_uxr_config_dialog_title)
+                .setMultiChoiceItems(UX_RESTRICTION_NAMES, null,
+                        (dialog, which, isChecked) -> selected[which] = isChecked)
+                .setPositiveButton(R.string.set_uxr_config_dialog_positive_button,
+                        (dialog, id) -> setUxRestrictionsConfig(selected))
+                .setNegativeButton(R.string.set_uxr_config_dialog_negative_button, null)
+                .show();
+    }
+
+    private void setUxRestrictionsConfig(boolean[] selected) {
+        int selectedRestrictions = 0;
+        // Iteration starts at 1 because 0 is BASELINE (no restrictions).
+        for (int i = 1; i < selected.length; i++) {
+            if (selected[i]) {
+                selectedRestrictions += 1 << (i - 1);
+            }
+        }
+        boolean reqOpt = selectedRestrictions != 0;
+        CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(DRIVING_STATE_PARKED, false, 0)
+                .setUxRestrictions(DRIVING_STATE_IDLING, reqOpt, selectedRestrictions)
+                .setUxRestrictions(DRIVING_STATE_MOVING, reqOpt, selectedRestrictions)
+                .build();
+
+        try {
+            mCarUxRestrictionsManager.saveUxRestrictionsConfigurationForNextBoot(config);
+        } catch (CarNotConnectedException e) {
+            Log.e(TAG, "Car not connected", e);
+        }
+    }
+
     private void launchSampleMsgActivity(View view) {
         Intent msgIntent = new Intent(this, SampleMessageActivity.class);
         startActivity(msgIntent);
     }
 
-
     @Override
     protected void onDestroy() {
+        super.onDestroy();
         try {
             if (mCarUxRestrictionsManager != null) {
                 mCarUxRestrictionsManager.unregisterListener();
diff --git a/tests/VmsSubscriberClientSample/src/com/google/android/car/vms/subscriber/VmsSubscriberClientSampleActivity.java b/tests/VmsSubscriberClientSample/src/com/google/android/car/vms/subscriber/VmsSubscriberClientSampleActivity.java
index 41de024..ca9915d 100644
--- a/tests/VmsSubscriberClientSample/src/com/google/android/car/vms/subscriber/VmsSubscriberClientSampleActivity.java
+++ b/tests/VmsSubscriberClientSample/src/com/google/android/car/vms/subscriber/VmsSubscriberClientSampleActivity.java
@@ -88,6 +88,14 @@
         @Override
         public void onServiceDisconnected(ComponentName name) {
             Log.d(TAG, "Disconnect from Car Service");
+            if (mVmsSubscriberManager != null) {
+                try {
+                    mVmsSubscriberManager.clearVmsSubscriberClientCallback();
+                    mVmsSubscriberManager.unsubscribe(TEST_LAYER);
+                } catch (android.car.CarNotConnectedException e) {
+                    Log.e(TAG, "Car is not connected!", e);
+                }
+            }
         }
 
         private VmsSubscriberManager getVmsSubscriberManager() {
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
index 5b3953f..ecc2e47 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarProjectionManagerTest.java
@@ -25,6 +25,7 @@
 import android.os.IBinder;
 import android.support.test.filters.RequiresDevice;
 import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -103,6 +104,8 @@
         mManager.unregisterProjectionRunner(intent);
     }
 
+    //TODO(b/120081013): move this test to CTS
+    @Suppress
     @RequiresDevice
     public void testAccessPoint() throws Exception {
         CountDownLatch startedLatch = new CountDownLatch(1);
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java b/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java
new file mode 100644
index 0000000..c55683a
--- /dev/null
+++ b/tests/android_car_api_test/src/android/car/apitest/CarUxRestrictionsConfigurationTest.java
@@ -0,0 +1,386 @@
+/*
+ * 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 android.car.apitest;
+
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_IDLING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_MOVING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED;
+import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
+import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED;
+import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO;
+import static android.car.drivingstate.CarUxRestrictionsConfiguration.Builder.SpeedRange.MAX_SPEED;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.support.test.filters.SmallTest;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+
+/**
+ * Unit test for UXR config and its subclasses.
+ */
+@SmallTest
+public class CarUxRestrictionsConfigurationTest extends TestCase {
+
+    // This test verifies the expected way to build config would succeed.
+    public void testConstruction() {
+        new CarUxRestrictionsConfiguration.Builder().build();
+
+        new CarUxRestrictionsConfiguration.Builder()
+                .setMaxStringLength(1)
+                .build();
+
+        new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
+                .build();
+
+        new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+                .build();
+
+        new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(DRIVING_STATE_MOVING,
+                        new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED),
+                        true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+                .build();
+
+        new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(DRIVING_STATE_MOVING,
+                        new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f),
+                        true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+                .setUxRestrictions(DRIVING_STATE_MOVING,
+                        new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f, MAX_SPEED),
+                        true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+                .build();
+    }
+
+    public void testUnspecifiedDrivingStateUsesDefaultRestriction() {
+        CarUxRestrictionsConfiguration config =
+                new CarUxRestrictionsConfiguration.Builder().build();
+
+        CarUxRestrictions parkedRestrictions = config.getUxRestrictions(DRIVING_STATE_PARKED, 0f);
+        assertTrue(parkedRestrictions.isRequiresDistractionOptimization());
+        assertEquals(parkedRestrictions.getActiveRestrictions(), UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+        CarUxRestrictions movingRestrictions = config.getUxRestrictions(DRIVING_STATE_MOVING, 1f);
+        assertTrue(movingRestrictions.isRequiresDistractionOptimization());
+        assertEquals(movingRestrictions.getActiveRestrictions(), UX_RESTRICTIONS_FULLY_RESTRICTED);
+    }
+
+    public void testBuilderValidation_MultipleSpeedRange_NonZeroStart() {
+        CarUxRestrictionsConfiguration.Builder builder =
+                new CarUxRestrictionsConfiguration.Builder();
+        builder.setUxRestrictions(DRIVING_STATE_MOVING,
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(1, 2),
+                true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+        builder.setUxRestrictions(DRIVING_STATE_MOVING,
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(2, MAX_SPEED),
+                true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+        try {
+            builder.build();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected exception.
+        }
+    }
+
+    public void testBuilderValidation_SpeedRange_NonZeroStart() {
+        CarUxRestrictionsConfiguration.Builder builder =
+                new CarUxRestrictionsConfiguration.Builder();
+        builder.setUxRestrictions(DRIVING_STATE_MOVING,
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(1, MAX_SPEED),
+                true, UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+        try {
+            builder.build();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected exception.
+        }
+    }
+
+    public void testBuilderValidation_SpeedRange_Overlap() {
+        CarUxRestrictionsConfiguration.Builder builder =
+                new CarUxRestrictionsConfiguration.Builder();
+        builder.setUxRestrictions(DRIVING_STATE_MOVING,
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+                UX_RESTRICTIONS_FULLY_RESTRICTED);
+        builder.setUxRestrictions(DRIVING_STATE_MOVING,
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(4), true,
+                UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+        try {
+            builder.build();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected exception.
+        }
+    }
+
+    public void testBuilderValidation_SpeedRange_Gap() {
+        CarUxRestrictionsConfiguration.Builder builder =
+                new CarUxRestrictionsConfiguration.Builder();
+        builder.setUxRestrictions(DRIVING_STATE_MOVING,
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+                UX_RESTRICTIONS_FULLY_RESTRICTED);
+        builder.setUxRestrictions(DRIVING_STATE_MOVING,
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(8), true,
+                UX_RESTRICTIONS_FULLY_RESTRICTED);
+
+        try {
+            builder.build();
+            fail();
+        } catch (IllegalStateException e) {
+            // Expected exception.
+        }
+    }
+
+    public void testBuilderValidation_NonMovingStateCannotUseSpeedRange() {
+        CarUxRestrictionsConfiguration.Builder builder =
+                new CarUxRestrictionsConfiguration.Builder();
+        try {
+            builder.setUxRestrictions(DRIVING_STATE_PARKED,
+                    new CarUxRestrictionsConfiguration.Builder.SpeedRange(0, 5), true,
+                    UX_RESTRICTIONS_FULLY_RESTRICTED);
+        } catch (IllegalArgumentException e) {
+            // Expected exception.
+        }
+    }
+
+    public void testSpeedRange_Construction() {
+        new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+        new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+        new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+    }
+
+    public void testSpeedRange_NegativeMax() {
+        try {
+            new CarUxRestrictionsConfiguration.Builder.SpeedRange(2f, -1f);
+        } catch (IllegalArgumentException e) {
+            // Expected exception.
+        }
+    }
+
+    public void testSpeedRange_MinGreaterThanMax() {
+        try {
+            new CarUxRestrictionsConfiguration.Builder.SpeedRange(5f, 2f);
+        } catch (IllegalArgumentException e) {
+            // Expected exception.
+        }
+    }
+
+    public void testSpeedRangeComparison_DifferentMin() {
+        CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+        CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(2f);
+        assertTrue(s1.compareTo(s2) < 0);
+        assertTrue(s2.compareTo(s1) > 0);
+    }
+
+    public void testSpeedRangeComparison_SameMin() {
+        CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+        CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+        assertTrue(s1.compareTo(s2) == 0);
+    }
+
+    public void testSpeedRangeComparison_SameMinDifferentMax() {
+        CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+        CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 2f);
+        assertTrue(s1.compareTo(s2) < 0);
+        assertTrue(s2.compareTo(s1) > 0);
+    }
+
+    public void testSpeedRangeComparison_MaxSpeed() {
+        CarUxRestrictionsConfiguration.Builder.SpeedRange s1 =
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+        CarUxRestrictionsConfiguration.Builder.SpeedRange s2 =
+                new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+        assertTrue(s1.compareTo(s2) < 0);
+        assertTrue(s2.compareTo(s1) > 0);
+    }
+
+    public void testSpeedRangeEquals() {
+        CarUxRestrictionsConfiguration.Builder.SpeedRange s1, s2;
+
+        s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+        assertTrue(s1.equals(s1));
+
+        s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+        s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+        assertTrue(s1.compareTo(s2) == 0);
+        assertTrue(s1.equals(s2));
+
+        s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+        s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+        assertTrue(s1.equals(s2));
+
+        s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+        s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, MAX_SPEED);
+        assertTrue(s1.equals(s2));
+
+        s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f);
+        s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f);
+        assertFalse(s1.equals(s2));
+
+        s1 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f);
+        s2 = new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 2f);
+        assertFalse(s1.equals(s2));
+    }
+
+    // TODO: add more tests that each verifies setting one filed in builder can be
+    // successfully retrieved out of saved json.
+    public void testJsonSerialization_DefaultConstructor() {
+        CarUxRestrictionsConfiguration config =
+                new CarUxRestrictionsConfiguration.Builder().build();
+
+        verifyConfigThroughJsonSerialization(config);
+    }
+
+    public void testJsonSerialization_RestrictionParameters() {
+        CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+                .setMaxStringLength(1)
+                .setMaxCumulativeContentItems(1)
+                .setMaxContentDepth(1)
+                .build();
+
+        verifyConfigThroughJsonSerialization(config);
+    }
+
+    public void testJsonSerialization_NonMovingStateRestrictions() {
+        CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
+                .build();
+
+        verifyConfigThroughJsonSerialization(config);
+    }
+
+    public void testJsonSerialization_MovingStateNoSpeedRange() {
+        CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+                .build();
+
+        verifyConfigThroughJsonSerialization(config);
+    }
+
+    public void testJsonSerialization_MovingStateWithSpeedRange() {
+        CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(DRIVING_STATE_MOVING,
+                        new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 5f),
+                        true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+                .setUxRestrictions(DRIVING_STATE_MOVING,
+                        new CarUxRestrictionsConfiguration.Builder.SpeedRange(5f, MAX_SPEED),
+                        true, UX_RESTRICTIONS_FULLY_RESTRICTED)
+                .build();
+
+        verifyConfigThroughJsonSerialization(config);
+    }
+
+    public void testDump() {
+        CarUxRestrictionsConfiguration[] configs = new CarUxRestrictionsConfiguration[] {
+                // Driving state with no speed range
+                new CarUxRestrictionsConfiguration.Builder()
+                        .setUxRestrictions(DRIVING_STATE_PARKED, false, UX_RESTRICTIONS_BASELINE)
+                        .setUxRestrictions(DRIVING_STATE_IDLING, true, UX_RESTRICTIONS_NO_VIDEO)
+                        .setUxRestrictions(DRIVING_STATE_MOVING, true, UX_RESTRICTIONS_NO_VIDEO)
+                        .build(),
+                // Parameters
+                new CarUxRestrictionsConfiguration.Builder()
+                        .setMaxStringLength(1)
+                        .setMaxContentDepth(1)
+                        .setMaxCumulativeContentItems(1)
+                        .build(),
+                // Driving state with single speed range
+                new CarUxRestrictionsConfiguration.Builder()
+                        .setUxRestrictions(DRIVING_STATE_MOVING,
+                                new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f),
+                                true, UX_RESTRICTIONS_NO_VIDEO)
+                        .build(),
+                // Driving state with multiple speed ranges
+                new CarUxRestrictionsConfiguration.Builder()
+                        .setUxRestrictions(DRIVING_STATE_MOVING,
+                                new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f),
+                                true, UX_RESTRICTIONS_NO_VIDEO)
+                        .setUxRestrictions(DRIVING_STATE_MOVING,
+                                new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f),
+                                true, UX_RESTRICTIONS_NO_VIDEO)
+                        .build(),
+        };
+
+        for (CarUxRestrictionsConfiguration config : configs) {
+            config.dump(new PrintWriter(new ByteArrayOutputStream()));
+        }
+    }
+
+    public void testDumpContainsNecessaryInfo() {
+
+        CarUxRestrictionsConfiguration config = new CarUxRestrictionsConfiguration.Builder()
+                .setUxRestrictions(DRIVING_STATE_MOVING,
+                        new CarUxRestrictionsConfiguration.Builder.SpeedRange(0f, 1f),
+                        true, UX_RESTRICTIONS_NO_VIDEO)
+                .setUxRestrictions(DRIVING_STATE_MOVING,
+                        new CarUxRestrictionsConfiguration.Builder.SpeedRange(1f),
+                        true, UX_RESTRICTIONS_NO_VIDEO)
+                .build();
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        try (PrintWriter writer = new PrintWriter(output)) {
+            config.dump(writer);
+        }
+
+        String dump = new String(output.toByteArray());
+        assertTrue(dump.contains("Max String length"));
+        assertTrue(dump.contains("Max Cumulative Content Items"));
+        assertTrue(dump.contains("Max Content depth"));
+        assertTrue(dump.contains("State:moving"));
+        assertTrue(dump.contains("Speed Range"));
+        assertTrue(dump.contains("Requires DO?"));
+        assertTrue(dump.contains("Restrictions"));
+    }
+
+    private void verifyConfigThroughJsonSerialization(CarUxRestrictionsConfiguration config) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(out))) {
+            config.writeJson(writer);
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail();
+        }
+
+        ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+        try (JsonReader reader = new JsonReader(new InputStreamReader(in))) {
+            CarUxRestrictionsConfiguration deserialized = CarUxRestrictionsConfiguration.readJson(
+                    reader);
+            assertTrue(config.equals(deserialized));
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail();
+        }
+    }
+}
diff --git a/tests/carservice_test/Android.mk b/tests/carservice_test/Android.mk
index 0270748..7c17e7e 100644
--- a/tests/carservice_test/Android.mk
+++ b/tests/carservice_test/Android.mk
@@ -20,7 +20,8 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_RESOURCE_DIR += packages/services/Car/service/res
+LOCAL_RESOURCE_DIR += $(LOCAL_PATH)/res \
+    packages/services/Car/service/res
 
 LOCAL_AAPT_FLAGS += --extra-packages com.android.car --auto-add-overlay
 
diff --git a/tests/carservice_test/res/xml/ux_restrictions_moving_state_multi_speed_range.xml b/tests/carservice_test/res/xml/ux_restrictions_moving_state_multi_speed_range.xml
new file mode 100644
index 0000000..9dd3678
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_moving_state_multi_speed_range.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+    <RestrictionMapping>
+        <DrivingState car:state="moving" car:minSpeed="0" car:maxSpeed="5.0">
+            <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_video"/>
+        </DrivingState>
+        <!-- Restrictions for speed >=5 -->
+        <DrivingState car:state="moving" car:minSpeed="5.0">
+            <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_keyboard"/>
+        </DrivingState>
+    </RestrictionMapping>
+</UxRestrictions>
diff --git a/tests/carservice_test/res/xml/ux_restrictions_moving_state_no_speed_range.xml b/tests/carservice_test/res/xml/ux_restrictions_moving_state_no_speed_range.xml
new file mode 100644
index 0000000..405619e
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_moving_state_no_speed_range.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+    <RestrictionMapping>
+        <DrivingState car:state="moving">
+            <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_video"/>
+        </DrivingState>
+    </RestrictionMapping>
+</UxRestrictions>
diff --git a/tests/carservice_test/res/xml/ux_restrictions_moving_state_single_speed_range.xml b/tests/carservice_test/res/xml/ux_restrictions_moving_state_single_speed_range.xml
new file mode 100644
index 0000000..4cd11b0
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_moving_state_single_speed_range.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+    <RestrictionMapping>
+        <DrivingState car:state="moving" car:minSpeed="0">
+            <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_video"/>
+        </DrivingState>
+    </RestrictionMapping>
+</UxRestrictions>
diff --git a/tests/carservice_test/res/xml/ux_restrictions_non_moving_state.xml b/tests/carservice_test/res/xml/ux_restrictions_non_moving_state.xml
new file mode 100644
index 0000000..cc49260
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_non_moving_state.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+    <RestrictionMapping>
+        <DrivingState car:state="parked">
+            <Restrictions car:requiresDistractionOptimization="false" car:uxr="baseline"/>
+        </DrivingState>
+        <DrivingState car:state="idling">
+            <Restrictions car:requiresDistractionOptimization="true" car:uxr="no_video"/>
+        </DrivingState>
+    </RestrictionMapping>
+</UxRestrictions>
diff --git a/tests/carservice_test/res/xml/ux_restrictions_only_parameters.xml b/tests/carservice_test/res/xml/ux_restrictions_only_parameters.xml
new file mode 100644
index 0000000..4c30c16
--- /dev/null
+++ b/tests/carservice_test/res/xml/ux_restrictions_only_parameters.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+This xml contains UX restrictions configuration for testing.
+-->
+
+<UxRestrictions xmlns:car="http://schemas.android.com/apk/res-auto">
+    <!-- restriction parameters -->
+    <RestrictionParameters>
+        <StringRestrictions car:maxLength="1"/>
+        <ContentRestrictions car:maxCumulativeItems="1" car:maxDepth="1"/>
+    </RestrictionParameters>
+</UxRestrictions>
diff --git a/tests/carservice_test/src/com/android/car/CarUxRestrictionsConfigurationXmlParserTest.java b/tests/carservice_test/src/com/android/car/CarUxRestrictionsConfigurationXmlParserTest.java
new file mode 100644
index 0000000..02000a3
--- /dev/null
+++ b/tests/carservice_test/src/com/android/car/CarUxRestrictionsConfigurationXmlParserTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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;
+
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_IDLING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_MOVING;
+import static android.car.drivingstate.CarDrivingStateEvent.DRIVING_STATE_PARKED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class CarUxRestrictionsConfigurationXmlParserTest {
+    private Context getContext() {
+        return InstrumentationRegistry.getTargetContext();
+    }
+
+    @Test
+    public void testParsingDefaultConfiguration() throws IOException, XmlPullParserException {
+        CarUxRestrictionsConfigurationXmlParser.parse(getContext(), R.xml.car_ux_restrictions_map);
+    }
+
+    @Test
+    public void testParsingParameters() throws IOException, XmlPullParserException {
+        CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+                getContext(), R.xml.ux_restrictions_only_parameters);
+
+        CarUxRestrictions r = config.getUxRestrictions(DRIVING_STATE_PARKED, 0f);
+        assertEquals(1, r.getMaxContentDepth());
+        assertEquals(1, r.getMaxCumulativeContentItems());
+        assertEquals(1, r.getMaxRestrictedStringLength());
+    }
+
+    @Test
+    public void testParsingNonMovingState() throws IOException, XmlPullParserException {
+        CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+                getContext(), R.xml.ux_restrictions_non_moving_state);
+
+        CarUxRestrictions parked = config.getUxRestrictions(DRIVING_STATE_PARKED, 0f);
+        assertFalse(parked.isRequiresDistractionOptimization());
+
+        CarUxRestrictions idling = config.getUxRestrictions(DRIVING_STATE_IDLING, 0f);
+        assertTrue(idling.isRequiresDistractionOptimization());
+        assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, idling.getActiveRestrictions());
+    }
+
+    @Test
+    public void testParsingMovingState_NoSpeedRange() throws IOException, XmlPullParserException {
+        CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+                getContext(), R.xml.ux_restrictions_moving_state_no_speed_range);
+
+        CarUxRestrictions r = config.getUxRestrictions(DRIVING_STATE_MOVING, 1f);
+        assertTrue(r.isRequiresDistractionOptimization());
+        assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, r.getActiveRestrictions());
+    }
+
+    @Test
+    public void testParsingMovingState_SingleSpeedRange()
+            throws IOException, XmlPullParserException {
+        CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+                getContext(), R.xml.ux_restrictions_moving_state_single_speed_range);
+
+        CarUxRestrictions r = config.getUxRestrictions(DRIVING_STATE_MOVING, 1f);
+        assertTrue(r.isRequiresDistractionOptimization());
+        assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, r.getActiveRestrictions());
+    }
+
+    @Test
+    public void testParsingMovingState_MultiSpeedRange()
+            throws IOException, XmlPullParserException {
+        CarUxRestrictionsConfiguration config = CarUxRestrictionsConfigurationXmlParser.parse(
+                getContext(), R.xml.ux_restrictions_moving_state_single_speed_range);
+
+        CarUxRestrictions slow = config.getUxRestrictions(DRIVING_STATE_MOVING, 1f);
+        assertTrue(slow.isRequiresDistractionOptimization());
+        assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, slow.getActiveRestrictions());
+
+        CarUxRestrictions fast = config.getUxRestrictions(DRIVING_STATE_MOVING, 6f);
+        assertTrue(fast.isRequiresDistractionOptimization());
+        assertEquals(CarUxRestrictions.UX_RESTRICTIONS_NO_VIDEO, fast.getActiveRestrictions());
+    }
+}
diff --git a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
new file mode 100644
index 0000000..e87badf
--- /dev/null
+++ b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
@@ -0,0 +1,235 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.CarUxRestrictionsConfiguration;
+import android.car.hardware.CarPropertyValue;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class CarUxRestrictionsManagerServiceTest {
+    private CarUxRestrictionsManagerService mService;
+
+    @Mock
+    private CarDrivingStateService mMockDrivingStateService;
+    @Mock
+    private CarPropertyService mMockCarPropertyService;
+    @Mock
+    private CarUserManagerHelper mMockCarUserManagerHelper;
+
+    private Context mSpyContext;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mSpyContext = spy(InstrumentationRegistry.getTargetContext());
+
+        setUpMockParkedState();
+
+        mService = new CarUxRestrictionsManagerService(mSpyContext,
+                mMockDrivingStateService, mMockCarPropertyService, mMockCarUserManagerHelper);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mService = null;
+    }
+
+    @Test
+    public void testSaveConfig_WriteStagedFile() throws Exception {
+        // Reset spy context because service tries to access production config
+        // during construction (due to calling loadConfig()).
+        reset(mSpyContext);
+
+        File staged = setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_STAGED, null);
+        CarUxRestrictionsConfiguration config = createEmptyConfig();
+
+        assertTrue(mService.saveUxRestrictionsConfigurationForNextBoot(config));
+        assertTrue(readFile(staged).equals(config));
+        // Verify prod config file was not touched.
+        verify(mSpyContext, never()).getFileStreamPath(
+                CarUxRestrictionsManagerService.CONFIG_FILENAME_PRODUCTION);
+    }
+
+    @Test
+    public void testSaveConfig_ReturnFalseOnException() throws Exception {
+        File tempFile = File.createTempFile("uxr_test", null);
+        doReturn(tempFile).when(mSpyContext).getFileStreamPath(anyString());
+
+        CarUxRestrictionsConfiguration spyConfig = spy(createEmptyConfig());
+        doThrow(new IOException()).when(spyConfig).writeJson(any(JsonWriter.class));
+
+        assertFalse(mService.saveUxRestrictionsConfigurationForNextBoot(spyConfig));
+    }
+
+    @Test
+    public void testSaveConfig_DoesNotAffectCurrentConfig() throws Exception {
+        File tempFile = File.createTempFile("uxr_test", null);
+        doReturn(tempFile).when(mSpyContext).getFileStreamPath(anyString());
+        CarUxRestrictionsConfiguration spyConfig = spy(createEmptyConfig());
+
+        CarUxRestrictionsConfiguration currentConfig = mService.getConfig();
+        assertTrue(mService.saveUxRestrictionsConfigurationForNextBoot(spyConfig));
+
+        verify(spyConfig).writeJson(any(JsonWriter.class));
+        // Verify current config is untouched by address comparison.
+        assertTrue(mService.getConfig() == currentConfig);
+    }
+
+    @Test
+    public void testLoadConfig_UseDefaultConfigWhenNoSavedConfigFileNoXml() {
+        // Prevent R.xml.car_ux_restrictions_map being returned.
+        Resources spyResources = spy(mSpyContext.getResources());
+        doReturn(spyResources).when(mSpyContext).getResources();
+        doReturn(null).when(spyResources).getXml(anyInt());
+
+        assertTrue(mService.loadConfig().equals(mService.createDefaultConfig()));
+    }
+
+    @Test
+    public void testLoadConfig_UseXml() throws IOException, XmlPullParserException {
+        CarUxRestrictionsConfiguration expected = CarUxRestrictionsConfigurationXmlParser.parse(
+                mSpyContext, R.xml.car_ux_restrictions_map);
+
+        CarUxRestrictionsConfiguration actual = mService.loadConfig();
+
+        assertTrue(actual.equals(expected));
+    }
+
+    @Test
+    public void testLoadConfig_UseProdConfig() throws IOException {
+        CarUxRestrictionsConfiguration expected = createEmptyConfig();
+        setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_PRODUCTION, expected);
+
+        CarUxRestrictionsConfiguration actual = mService.loadConfig();
+
+        assertTrue(actual.equals(expected));
+    }
+
+    @Test
+    public void testLoadConfig_PromoteStagedFileWhenParked() throws Exception {
+        CarUxRestrictionsConfiguration expected = createEmptyConfig();
+        // Staged file contains actual config. Ignore prod since it should be overwritten by staged.
+        File staged = setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_STAGED,
+                expected);
+        // Set up temp file for prod to avoid polluting other tests.
+        setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_PRODUCTION, null);
+
+        CarUxRestrictionsConfiguration actual = mService.loadConfig();
+
+        // Staged file should be moved as production.
+        assertFalse(staged.exists());
+        assertTrue(actual.equals(expected));
+    }
+
+    @Test
+    public void testLoadConfig_NoPromoteStagedFileWhenMoving() throws Exception {
+        CarUxRestrictionsConfiguration expected = createEmptyConfig();
+        File staged = setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_STAGED, null);
+        // Prod file contains actual config. Ignore staged since it should not be promoted.
+        setupMockFile(CarUxRestrictionsManagerService.CONFIG_FILENAME_PRODUCTION, expected);
+
+        setUpMockDrivingState();
+        CarUxRestrictionsConfiguration actual = mService.loadConfig();
+
+        // Staged file should be untouched.
+        assertTrue(staged.exists());
+        assertTrue(actual.equals(expected));
+    }
+
+    private CarUxRestrictionsConfiguration createEmptyConfig() {
+        return new CarUxRestrictionsConfiguration.Builder().build();
+    }
+
+    private void setUpMockParkedState() {
+        when(mMockDrivingStateService.getCurrentDrivingState()).thenReturn(
+                new CarDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_PARKED, 0));
+
+        CarPropertyValue<Float> speed = new CarPropertyValue<>(VehicleProperty.PERF_VEHICLE_SPEED,
+                0, 0f);
+        when(mMockCarPropertyService.getProperty(VehicleProperty.PERF_VEHICLE_SPEED, 0))
+                .thenReturn(speed);
+    }
+
+    private void setUpMockDrivingState() {
+        when(mMockDrivingStateService.getCurrentDrivingState()).thenReturn(
+                new CarDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_MOVING, 0));
+
+        CarPropertyValue<Float> speed = new CarPropertyValue<>(VehicleProperty.PERF_VEHICLE_SPEED,
+                0, 30f);
+        when(mMockCarPropertyService.getProperty(VehicleProperty.PERF_VEHICLE_SPEED, 0))
+                .thenReturn(speed);
+    }
+
+    private File setupMockFile(String filename, CarUxRestrictionsConfiguration config)
+            throws IOException {
+        File f = File.createTempFile("uxr_test", null);
+        doReturn(f).when(mSpyContext).getFileStreamPath(filename);
+
+        if (config != null) {
+            try (JsonWriter writer = new JsonWriter(
+                    new OutputStreamWriter(new FileOutputStream(f), "UTF-8"))) {
+                config.writeJson(writer);
+            }
+        }
+        return f;
+    }
+
+    private CarUxRestrictionsConfiguration readFile(File file) throws Exception {
+        try (JsonReader reader = new JsonReader(
+                new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
+            return CarUxRestrictionsConfiguration.readJson(reader);
+        }
+    }
+}
diff --git a/tests/carservice_test/src/com/android/car/SystemActivityMonitoringServiceTest.java b/tests/carservice_test/src/com/android/car/SystemActivityMonitoringServiceTest.java
index f8a4f0a..4455113 100644
--- a/tests/carservice_test/src/com/android/car/SystemActivityMonitoringServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/SystemActivityMonitoringServiceTest.java
@@ -16,7 +16,6 @@
 package com.android.car;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
 import android.app.Activity;
@@ -37,11 +36,13 @@
 
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
 
 @RunWith(AndroidJUnit4.class)
 @MediumTest
 public class SystemActivityMonitoringServiceTest {
     private static final long ACTIVITY_TIME_OUT = 5000;
+    private static final long DEFAULT_TIMEOUT_SECONDS = 2;
 
     private SystemActivityMonitoringService mService;
     private Semaphore mActivityLaunchSemaphore = new Semaphore(0);
@@ -100,17 +101,30 @@
         ComponentName activityThatFinishesImmediately =
                 toComponentName(getTestContext(), ActivityThatFinishesImmediately.class);
         startActivity(getContext(), activityThatFinishesImmediately);
-        assertTrue(mActivityLaunchSemaphore.tryAcquire(2, TimeUnit.SECONDS));
+        waitUntil(() -> topTasksHasComponent(activityThatFinishesImmediately));
+        waitUntil(() -> !topTasksHasComponent(activityThatFinishesImmediately));
+    }
 
-        // We won't know if the stack changes, unless we launch another activity.
-        startActivity(getContext(), toComponentName(getTestContext(), ActivityA.class));
-        assertTrue(mActivityLaunchSemaphore.tryAcquire(2, TimeUnit.SECONDS));
 
-        for (TopTaskInfoContainer topTaskInfoContainer: mService.getTopTasks()) {
-            assertNotEquals(topTaskInfoContainer.topActivity, activityThatFinishesImmediately);
+    private void waitUntil(BooleanSupplier condition) throws Exception {
+        while (!condition.getAsBoolean()) {
+            boolean didAquire =
+                    mActivityLaunchSemaphore.tryAcquire(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+            if (!didAquire && !condition.getAsBoolean()) {
+                throw new RuntimeException("failed while waiting for condition to become true");
+            }
         }
     }
 
+    private boolean topTasksHasComponent(ComponentName component) {
+        for (TopTaskInfoContainer topTaskInfoContainer: mService.getTopTasks()) {
+            if (topTaskInfoContainer.topActivity.equals(component)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /** Activity that closes itself after some timeout to clean up the screen. */
     public static class TempActivity extends Activity {
         @Override
diff --git a/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java b/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java
index 1ef35f7..1f0237f 100644
--- a/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java
+++ b/tests/carservice_test/src/com/android/car/VmsPublisherSubscriberTest.java
@@ -28,6 +28,7 @@
 import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
 import android.hardware.automotive.vehicle.V2_0.VehiclePropertyChangeMode;
+import android.support.test.filters.FlakyTest;
 import android.support.test.filters.MediumTest;
 import android.support.test.runner.AndroidJUnit4;
 
@@ -47,6 +48,7 @@
 
 @RunWith(AndroidJUnit4.class)
 @MediumTest
+@FlakyTest // TODO(b/116333782): Remove the flag when issue fixed
 public class VmsPublisherSubscriberTest extends MockedCarTestBase {
     private static final int LAYER_ID = 88;
     private static final int LAYER_VERSION = 19;
diff --git a/tests/carservice_unit_test/src/com/android/car/BluetoothAutoConnectPolicyTest.java b/tests/carservice_unit_test/src/com/android/car/BluetoothAutoConnectPolicyTest.java
index a180886..8a27909 100644
--- a/tests/carservice_unit_test/src/com/android/car/BluetoothAutoConnectPolicyTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/BluetoothAutoConnectPolicyTest.java
@@ -64,7 +64,10 @@
  * and connection results can be injected (imitating results from the stack)
  * 2. {@link CarCabinService} & {@link CarSensorService} - Fake vehicle events are injected to the
  * policy's Broadcast Receiver.
+ *
+ * TODO(b/77825248): Fix the flakiness to enable the test class again
  */
+@Suppress
 public class BluetoothAutoConnectPolicyTest extends AndroidTestCase {
     private BluetoothDeviceConnectionPolicy mBluetoothDeviceConnectionPolicyTest;
     private BluetoothAdapter mBluetoothAdapter;
diff --git a/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java b/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java
index f731bec..4757015 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarUserManagerHelperTest.java
@@ -43,6 +43,7 @@
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.sysprop.CarProperties;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -109,7 +110,7 @@
 
         // Restore the non-headless state before every test. Individual tests can set the property
         // to true to test the headless system user scenario.
-        SystemProperties.set("android.car.systemuser.headless", "false");
+        CarProperties.headless_system_user(false);
     }
 
     @Test
@@ -126,7 +127,7 @@
     // System user will not be returned when calling get all users.
     @Test
     public void testHeadlessUser0GetAllUsers_NotReturnSystemUser() {
-        SystemProperties.set("android.car.systemuser.headless", "true");
+        CarProperties.headless_system_user(true);
         UserInfo otherUser1 = createUserInfoForId(10);
         UserInfo otherUser2 = createUserInfoForId(11);
         UserInfo otherUser3 = createUserInfoForId(12);
@@ -274,7 +275,7 @@
         assertThat(mCarUserManagerHelper.getMaxSupportedUsers()).isEqualTo(11);
 
         // In headless user 0 model, we want to exclude the system user.
-        SystemProperties.set("android.car.systemuser.headless", "true");
+        CarProperties.headless_system_user(true);
         assertThat(mCarUserManagerHelper.getMaxSupportedUsers()).isEqualTo(10);
     }
 
@@ -318,7 +319,7 @@
 
     @Test
     public void testHeadlessSystemUser_IsUserLimitReached() {
-        SystemProperties.set("android.car.systemuser.headless", "true");
+        CarProperties.headless_system_user(true);
         UserInfo user1 = createUserInfoForId(10);
         UserInfo user2 =
                 new UserInfo(/* id= */ 11, /* name = */ "user11", UserInfo.FLAG_MANAGED_PROFILE);
@@ -772,7 +773,7 @@
 
     @Test
     public void testGetInitialUserWithValidLastActiveUser() {
-        SystemProperties.set("android.car.systemuser.headless", "true");
+        CarProperties.headless_system_user(true);
         int lastActiveUserId = 12;
 
         UserInfo otherUser1 = createUserInfoForId(lastActiveUserId - 2);
@@ -787,7 +788,7 @@
 
     @Test
     public void testGetInitialUserWithNonExistLastActiveUser() {
-        SystemProperties.set("android.car.systemuser.headless", "true");
+        CarProperties.headless_system_user(true);
         int lastActiveUserId = 12;
 
         UserInfo otherUser1 = createUserInfoForId(lastActiveUserId - 2);
diff --git a/tools/keventreader/server/Android.mk b/tools/keventreader/server/Android.mk
index 726e2fc..3dd6bba 100644
--- a/tools/keventreader/server/Android.mk
+++ b/tools/keventreader/server/Android.mk
@@ -42,6 +42,5 @@
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_CFLAGS += -Wall -Werror
-LOCAL_CPPFLAGS += -std=c++17
 
 include $(BUILD_EXECUTABLE)
diff --git a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
index 227b2fb..649c89d 100644
--- a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
+++ b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
@@ -29,10 +29,10 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.sysprop.CarProperties;
 import android.util.Log;
 
 import com.android.internal.util.UserIcons;
@@ -56,7 +56,6 @@
  */
 public class CarUserManagerHelper {
     private static final String TAG = "CarUserManagerHelper";
-    private static final String HEADLESS_SYSTEM_USER = "android.car.systemuser.headless";
 
     // Place holder for user name of the first user created.
     public static final String DEFAULT_FIRST_ADMIN_NAME = "Driver";
@@ -259,7 +258,7 @@
      * @return {@boolean true} if headless system user.
      */
     public boolean isHeadlessSystemUser() {
-        return SystemProperties.getBoolean(HEADLESS_SYSTEM_USER, false);
+        return CarProperties.headless_system_user().orElse(false);
     }
 
     /**