Merge changes from topic "per_user_volume_settings" into rvc-dev

* changes:
  Update Volume Embedded Kitchen Sink Fragment.
  Fixed car audio volume group settings for user.
diff --git a/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java b/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
index 8dc2626..e5c5740 100644
--- a/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
+++ b/car-test-lib/src/android/car/test/mocks/AbstractExtendedMockitoTestCase.java
@@ -28,6 +28,7 @@
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.os.UserManager;
@@ -48,6 +49,7 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.quality.Strictness;
 import org.mockito.session.MockitoSessionBuilder;
+import org.mockito.stubbing.Answer;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
@@ -113,7 +115,7 @@
      * Adds key-value(int) pair in mocked Settings.Global and Settings.Secure
      */
     protected void putSettingsInt(@NonNull String key, int value) {
-        mSettings.insertInt(key, value);
+        mSettings.insertObject(key, value);
     }
 
     /**
@@ -127,7 +129,7 @@
      * Adds key-value(String) pair in mocked Settings.Global and Settings.Secure
      */
     protected void putSettingsString(@NonNull String key, @NonNull String value) {
-        mSettings.insertString(key, value);
+        mSettings.insertObject(key, value);
     }
 
     /**
@@ -234,6 +236,7 @@
         StaticMockitoSessionBuilder builder = mockitoSession()
                 .strictness(getSessionStrictness())
                 .mockStatic(Settings.Global.class)
+                .mockStatic(Settings.System.class)
                 .mockStatic(Settings.Secure.class);
 
         CustomMockitoSessionBuilder customBuilder =
@@ -313,70 +316,92 @@
     }
 
     // TODO (b/155523104): Add log
+    // TODO (b/156033195): Clean settings API
     private static final class MockSettings {
-        private HashMap<String, Integer> mIntMapping = new HashMap<String, Integer>();
-        private HashMap<String, String> mStringMapping = new HashMap<String, String>();
+        private static final int INVALID_DEFAULT_INDEX = -1;
+        private HashMap<String, Object> mSettingsMapping = new HashMap<>();
 
         MockSettings() {
-            when(Settings.Global.putInt(any(), any(), anyInt())).thenAnswer(invocation -> {
-                String key = (String) invocation.getArguments()[1];
-                int value = (int) invocation.getArguments()[2];
-                insertInt(key, value);
-                return null;
-            });
 
-            when(Settings.Global.getInt(any(), any(), anyInt())).thenAnswer(invocation -> {
-                String key = (String) invocation.getArguments()[1];
-                int defaultValue = (int) invocation.getArguments()[2];
-                return getInt(key, defaultValue);
-            });
+            Answer<Object> insertObjectAnswer =
+                    invocation -> insertObjectFromInvocation(invocation, 1, 2);
+            Answer<Integer> getIntAnswer = invocation ->
+                    getAnswer(invocation, Integer.class, 1, 2);
+            Answer<String> getStringAnswer = invocation ->
+                    getAnswer(invocation, String.class, 1, INVALID_DEFAULT_INDEX);
+
+
+            when(Settings.Global.putInt(any(), any(), anyInt())).thenAnswer(insertObjectAnswer);
+
+            when(Settings.Global.getInt(any(), any(), anyInt())).thenAnswer(getIntAnswer);
 
             when(Settings.Secure.putIntForUser(any(), any(), anyInt(), anyInt()))
-                    .thenAnswer(invocation -> {
-                        String key = (String) invocation.getArguments()[1];
-                        int value = (int) invocation.getArguments()[2];
-                        insertInt(key, value);
-                        return null;
-                    });
+                    .thenAnswer(insertObjectAnswer);
 
             when(Settings.Secure.getIntForUser(any(), any(), anyInt(), anyInt()))
-                    .thenAnswer(invocation -> {
-                        String key = (String) invocation.getArguments()[1];
-                        int defaultValue = (int) invocation.getArguments()[2];
-                        return getInt(key, defaultValue);
-                    });
+                    .thenAnswer(getIntAnswer);
 
-            when(Settings.Global.putString(any(), any(), any())).thenAnswer(invocation -> {
-                String key = (String) invocation.getArguments()[1];
-                String value = (String) invocation.getArguments()[2];
-                insertString(key, value);
-                return null;
-            });
+            when(Settings.Global.putString(any(), any(), any()))
+                    .thenAnswer(insertObjectAnswer);
 
-            when(Settings.Global.getString(any(), any())).thenAnswer(invocation -> {
-                String key = (String) invocation.getArguments()[1];
-                return getString(key);
-            });
+            when(Settings.Global.getString(any(), any())).thenAnswer(getStringAnswer);
+
+            when(Settings.System.putIntForUser(any(), any(), anyInt(), anyInt()))
+                    .thenAnswer(insertObjectAnswer);
+
+            when(Settings.System.getIntForUser(any(), any(), anyInt(), anyInt()))
+                    .thenAnswer(getIntAnswer);
         }
 
-        public void insertInt(String key, int value) {
-            mIntMapping.put(key, value);
+        private Object insertObjectFromInvocation(InvocationOnMock invocation,
+                int keyIndex, int valueIndex) {
+            String key = (String) invocation.getArguments()[keyIndex];
+            Object value = invocation.getArguments()[valueIndex];
+            insertObject(key, value);
+            return null;
+        }
+
+        private void insertObject(String key, Object value) {
+            if (VERBOSE) Log.v(TAG, "Inserting Setting " + key + ": " + value);
+            mSettingsMapping.put(key, value);
+        }
+
+        private <T> T getAnswer(InvocationOnMock invocation, Class<T> clazz,
+                int keyIndex, int defaultValueIndex) {
+            String key = (String) invocation.getArguments()[keyIndex];
+            T defaultValue = null;
+            if (defaultValueIndex > INVALID_DEFAULT_INDEX) {
+                defaultValue = safeCast(invocation.getArguments()[defaultValueIndex], clazz);
+            }
+            return get(key, defaultValue, clazz);
+        }
+
+        @Nullable
+        private <T> T get(String key, T defaultValue, Class<T> clazz) {
+            if (VERBOSE) Log.v(TAG, "Getting Setting " + key);
+            Object value = mSettingsMapping.get(key);
+            if (value == null) {
+                return defaultValue;
+            }
+            return safeCast(value, clazz);
+        }
+
+        private <T> T safeCast(Object value, Class<T> clazz) {
+            if (value == null) {
+                return null;
+            }
+            Preconditions.checkArgument(value.getClass() == clazz,
+                    "Setting value has class %s but requires class %s",
+                    value.getClass(), clazz);
+            return (T) value;
+        }
+
+        private String getString(String key) {
+            return get(key, null, String.class);
         }
 
         public int getInt(String key) {
-            return mIntMapping.get(key);
-        }
-
-        public int getInt(String key, int defaultValue) {
-            return mIntMapping.getOrDefault(key, defaultValue);
-        }
-
-        public void insertString(String key, String value) {
-            mStringMapping.put(key, value);
-        }
-
-        public String getString(String key) {
-            return mStringMapping.get(key);
+            return (int) get(key, null, Integer.class);
         }
     }
 
diff --git a/service/src/com/android/car/CarOccupantZoneService.java b/service/src/com/android/car/CarOccupantZoneService.java
index 545c636..d9949c0 100644
--- a/service/src/com/android/car/CarOccupantZoneService.java
+++ b/service/src/com/android/car/CarOccupantZoneService.java
@@ -551,6 +551,13 @@
     }
 
     /**
+     * returns the current driver user id.
+     */
+    public @UserIdInt int getDriverUserId() {
+        return getCurrentUser();
+    }
+
+    /**
      * Sets the mapping for audio zone id to occupant zone id.
      *
      * @param audioZoneIdToOccupantZoneMapping map for audio zone id, where key is the audio zone id
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index 6cf9cd8..c880709 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -20,7 +20,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.app.ActivityManager;
 import android.car.Car;
 import android.car.CarOccupantZoneManager;
 import android.car.CarOccupantZoneManager.OccupantZoneConfigChangeListener;
@@ -1151,33 +1150,39 @@
     }
 
     private void handleOccupantZoneUserChanged() {
+        int driverUserId = mOccupantZoneService.getDriverUserId();
         synchronized (mImplLock) {
-            if (isOccupantZoneMappingAvailable()) {
+            if (!isOccupantZoneMappingAvailable()) {
                 //No occupant zone to audio zone mapping, re-adjust to settings driver.
-                int driverId = ActivityManager.getCurrentUser();
                 for (int index = 0; index < mCarAudioZones.length; index++) {
                     CarAudioZone zone = mCarAudioZones[index];
-                    zone.updateVolumeGroupsForUser(driverId);
-                    mFocusHandler.updateUserForZoneId(zone.getId(), driverId);
+                    zone.updateVolumeGroupsForUser(driverUserId);
+                    mFocusHandler.updateUserForZoneId(zone.getId(), driverUserId);
                 }
                 return;
             }
             for (int index = 0; index < mAudioZoneIdToOccupantZoneIdMapping.size(); index++) {
                 int audioZoneId = mAudioZoneIdToOccupantZoneIdMapping.keyAt(index);
                 int occupantZoneId = mAudioZoneIdToOccupantZoneIdMapping.get(audioZoneId);
-                updateUserForOccupantZoneLocked(occupantZoneId, audioZoneId);
+                updateUserForOccupantZoneLocked(occupantZoneId, audioZoneId, driverUserId);
             }
         }
     }
 
     private boolean isOccupantZoneMappingAvailable() {
-        return mAudioZoneIdToOccupantZoneIdMapping.size() == 0;
+        return mAudioZoneIdToOccupantZoneIdMapping.size() > 0;
     }
 
-    private void updateUserForOccupantZoneLocked(int occupantZoneId, int audioZoneId) {
+    private void updateUserForOccupantZoneLocked(int occupantZoneId, int audioZoneId,
+            @UserIdInt int driverUserId) {
+        CarAudioZone zone = getAudioZoneForZoneIdLocked(audioZoneId);
         int userId = mOccupantZoneService.getUserForOccupant(occupantZoneId);
         int prevUserId = getUserIdForZoneLocked(audioZoneId);
 
+        Objects.requireNonNull(zone, () ->
+                "setUserIdDeviceAffinity for userId " + userId
+                        + " in zone " + audioZoneId + " Failed, invalid zone.");
+
         // user in occupant zone has not changed
         if (userId == prevUserId) {
             return;
@@ -1186,30 +1191,30 @@
         // This would be true even if the new user is UserHandle.USER_NULL,
         // as that indicates the user has logged out.
         removeUserIdDeviceAffinitiesLocked(prevUserId);
-        resetCarZonesAudioFocus(audioZoneId);
 
         if (userId == UserHandle.USER_NULL) {
+            // Reset zone back to driver user id
+            resetZoneToDefaultUser(zone, driverUserId);
             return;
         }
-        CarAudioZone zone = getAudioZoneForZoneIdLocked(audioZoneId);
-        if (zone != null
-                && !mAudioPolicy.setUserIdDeviceAffinity(userId, zone.getAudioDeviceInfos())) {
+        if (!mAudioPolicy.setUserIdDeviceAffinity(userId, zone.getAudioDeviceInfos())) {
             throw new IllegalStateException(String.format(
                     "setUserIdDeviceAffinity for userId %d in zone %d Failed,"
                             + " could not set audio routing.",
                     userId, audioZoneId));
-        } else if (zone == null) {
-            throw new IllegalStateException(String.format(
-                    "setUserIdDeviceAffinity for userId %d in zone %d Failed, invalid zone.",
-                    userId, audioZoneId));
         }
         mAudioZoneIdToUserIdMapping.put(audioZoneId, userId);
         zone.updateVolumeGroupsForUser(userId);
         mFocusHandler.updateUserForZoneId(audioZoneId, userId);
     }
 
-    private void resetCarZonesAudioFocus(int audioZoneId) {
-        mFocusHandler.updateUserForZoneId(audioZoneId, UserHandle.USER_NULL);
+    private void resetZoneToDefaultUser(CarAudioZone zone, @UserIdInt int driverUserId) {
+        resetCarZonesAudioFocus(zone.getId(), driverUserId);
+        zone.updateVolumeGroupsForUser(driverUserId);
+    }
+
+    private void resetCarZonesAudioFocus(int audioZoneId, @UserIdInt int driverUserId) {
+        mFocusHandler.updateUserForZoneId(audioZoneId, driverUserId);
     }
 
     private CarAudioZone getAudioZoneForZoneIdLocked(int audioZoneId) {
diff --git a/service/src/com/android/car/audio/CarVolumeGroup.java b/service/src/com/android/car/audio/CarVolumeGroup.java
index 35d5a46..5244b67 100644
--- a/service/src/com/android/car/audio/CarVolumeGroup.java
+++ b/service/src/com/android/car/audio/CarVolumeGroup.java
@@ -17,10 +17,11 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ActivityManager;
+import android.annotation.UserIdInt;
 import android.car.media.CarAudioManager;
 import android.content.Context;
 import android.media.AudioDevicePort;
+import android.os.UserHandle;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -49,12 +50,15 @@
     private final SparseArray<String> mContextToAddress = new SparseArray<>();
     private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo = new HashMap<>();
 
+    private final Object mLock = new Object();
+
     private int mDefaultGain = Integer.MIN_VALUE;
     private int mMaxGain = Integer.MIN_VALUE;
     private int mMinGain = Integer.MAX_VALUE;
     private int mStepSize = 0;
     private int mStoredGainIndex;
     private int mCurrentGainIndex = -1;
+    private @UserIdInt int mUserId = UserHandle.USER_CURRENT;
 
     /**
      * Constructs a {@link CarVolumeGroup} instance
@@ -66,8 +70,7 @@
         mSettingsManager = settings;
         mZoneId = zoneId;
         mId = id;
-
-        updateUserId(ActivityManager.getCurrentUser());
+        mStoredGainIndex = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId, mId);
     }
 
     /**
@@ -152,28 +155,31 @@
                         CarAudioContext.toString(carAudioContext),
                         mContextToAddress.get(carAudioContext)));
 
-        if (mAddressToCarAudioDeviceInfo.size() == 0) {
-            mStepSize = info.getStepValue();
-        } else {
-            Preconditions.checkArgument(
-                    info.getStepValue() == mStepSize,
-                    "Gain controls within one group must have same step value");
-        }
+        synchronized (mLock) {
+            if (mAddressToCarAudioDeviceInfo.size() == 0) {
+                mStepSize = info.getStepValue();
+            } else {
+                Preconditions.checkArgument(
+                        info.getStepValue() == mStepSize,
+                        "Gain controls within one group must have same step value");
+            }
 
-        mAddressToCarAudioDeviceInfo.put(info.getAddress(), info);
-        mContextToAddress.put(carAudioContext, info.getAddress());
+            mAddressToCarAudioDeviceInfo.put(info.getAddress(), info);
+            mContextToAddress.put(carAudioContext, info.getAddress());
 
-        if (info.getDefaultGain() > mDefaultGain) {
-            // We're arbitrarily selecting the highest device default gain as the group's default.
-            mDefaultGain = info.getDefaultGain();
+            if (info.getDefaultGain() > mDefaultGain) {
+                // We're arbitrarily selecting the highest
+                // device default gain as the group's default.
+                mDefaultGain = info.getDefaultGain();
+            }
+            if (info.getMaxGain() > mMaxGain) {
+                mMaxGain = info.getMaxGain();
+            }
+            if (info.getMinGain() < mMinGain) {
+                mMinGain = info.getMinGain();
+            }
+            updateCurrentGainIndexLocked();
         }
-        if (info.getMaxGain() > mMaxGain) {
-            mMaxGain = info.getMaxGain();
-        }
-        if (info.getMinGain() < mMinGain) {
-            mMinGain = info.getMinGain();
-        }
-        updateCurrentGainIndex();
     }
 
     /**
@@ -181,42 +187,60 @@
      * @param userId new user
      * @note also reloads the store gain index for the user
      */
-    private void updateUserId(int userId) {
-        mStoredGainIndex = mSettingsManager.getStoredVolumeGainIndexForUser(userId, mZoneId, mId);
-        Log.i(CarLog.TAG_AUDIO, "updateUserId userId " + userId
-                + " mStoredGainIndex " + mStoredGainIndex);
+    private void updateUserIdLocked(@UserIdInt int userId) {
+        mUserId = userId;
+        mStoredGainIndex = getCurrentGainIndexForUserLocked();
+    }
+
+    private int getCurrentGainIndexForUserLocked() {
+        int gainIndexForUser = mSettingsManager.getStoredVolumeGainIndexForUser(mUserId, mZoneId,
+                mId);
+        Log.i(CarLog.TAG_AUDIO, "updateUserId userId " + mUserId
+                + " gainIndexForUser " + gainIndexForUser);
+        return gainIndexForUser;
     }
 
     /**
      * Update the current gain index based on the stored gain index
      */
-    private void updateCurrentGainIndex() {
-        if (mStoredGainIndex < getMinGainIndex() || mStoredGainIndex > getMaxGainIndex()) {
-            // We expected to load a value from last boot, but if we didn't (perhaps this is the
-            // first boot ever?), then use the highest "default" we've seen to initialize
-            // ourselves.
-            mCurrentGainIndex = getIndexForGain(mDefaultGain);
-        } else {
-            // Just use the gain index we stored last time the gain was set (presumably during our
-            // last boot cycle).
-            mCurrentGainIndex = mStoredGainIndex;
+    private void updateCurrentGainIndexLocked() {
+        synchronized (mLock) {
+            if (mStoredGainIndex < getIndexForGainLocked(mMinGain)
+                    || mStoredGainIndex > getIndexForGainLocked(mMaxGain)) {
+                // We expected to load a value from last boot, but if we didn't (perhaps this is the
+                // first boot ever?), then use the highest "default" we've seen to initialize
+                // ourselves.
+                mCurrentGainIndex = getIndexForGainLocked(mDefaultGain);
+            } else {
+                // Just use the gain index we stored last time the gain was
+                // set (presumably during our last boot cycle).
+                mCurrentGainIndex = mStoredGainIndex;
+            }
         }
     }
 
     private int getDefaultGainIndex() {
-        return getIndexForGain(mDefaultGain);
+        synchronized (mLock) {
+            return getIndexForGainLocked(mDefaultGain);
+        }
     }
 
     int getMaxGainIndex() {
-        return getIndexForGain(mMaxGain);
+        synchronized (mLock) {
+            return getIndexForGainLocked(mMaxGain);
+        }
     }
 
     int getMinGainIndex() {
-        return getIndexForGain(mMinGain);
+        synchronized (mLock) {
+            return getIndexForGainLocked(mMinGain);
+        }
     }
 
     int getCurrentGainIndex() {
-        return mCurrentGainIndex;
+        synchronized (mLock) {
+            return mCurrentGainIndex;
+        }
     }
 
     /**
@@ -224,37 +248,43 @@
      * @param gainIndex The gain index
      */
     void setCurrentGainIndex(int gainIndex) {
-        int gainInMillibels = getGainForIndex(gainIndex);
+        synchronized (mLock) {
+            int gainInMillibels = getGainForIndexLocked(gainIndex);
+            Preconditions.checkArgument(
+                    gainInMillibels >= mMinGain && gainInMillibels <= mMaxGain,
+                    "Gain out of range ("
+                            + mMinGain + ":"
+                            + mMaxGain + ") "
+                            + gainInMillibels + "index "
+                            + gainIndex);
 
-        Preconditions.checkArgument(
-                gainInMillibels >= mMinGain && gainInMillibels <= mMaxGain,
-                "Gain out of range ("
-                        + mMinGain + ":"
-                        + mMaxGain + ") "
-                        + gainInMillibels + "index "
-                        + gainIndex);
+            for (String address : mAddressToCarAudioDeviceInfo.keySet()) {
+                CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
+                info.setCurrentGain(gainInMillibels);
+            }
 
-        for (String address : mAddressToCarAudioDeviceInfo.keySet()) {
-            CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
-            info.setCurrentGain(gainInMillibels);
+            mCurrentGainIndex = gainIndex;
+
+            storeGainIndexForUserLocked(mCurrentGainIndex, mUserId);
         }
+    }
 
-        mCurrentGainIndex = gainIndex;
-        mSettingsManager.storeVolumeGainIndexForUser(ActivityManager.getCurrentUser(),
+    private void storeGainIndexForUserLocked(int gainIndex, @UserIdInt int userId) {
+        mSettingsManager.storeVolumeGainIndexForUser(userId,
                 mZoneId, mId, gainIndex);
     }
 
     // Given a group level gain index, return the computed gain in millibells
     // TODO (randolphs) If we ever want to add index to gain curves other than lock-stepped
     // linear, this would be the place to do it.
-    private int getGainForIndex(int gainIndex) {
+    private int getGainForIndexLocked(int gainIndex) {
         return mMinGain + gainIndex * mStepSize;
     }
 
     // TODO (randolphs) if we ever went to a non-linear index to gain curve mapping, we'd need to
     // revisit this as it assumes (at the least) that getGainForIndex is reversible.  Luckily,
     // this is an internal implementation details we could factor out if/when necessary.
-    private int getIndexForGain(int gainInMillibel) {
+    private int getIndexForGainLocked(int gainInMillibel) {
         return (gainInMillibel - mMinGain) / mStepSize;
     }
 
@@ -281,37 +311,41 @@
 
     /** Writes to dumpsys output */
     void dump(String indent, PrintWriter writer) {
-        writer.printf("%sCarVolumeGroup(%d)\n", indent, mId);
-        writer.printf("%sUserId(%d)\n", indent, ActivityManager.getCurrentUser());
-        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 < mContextToAddress.size(); i++) {
-            writer.printf("%sContext: %s -> Address: %s\n", indent,
-                    CarAudioContext.toString(mContextToAddress.keyAt(i)),
-                    mContextToAddress.valueAt(i));
-        }
-        mAddressToCarAudioDeviceInfo.keySet().stream()
-                .map(mAddressToCarAudioDeviceInfo::get)
-                .forEach((info -> info.dump(indent, writer)));
+        synchronized (mLock) {
+            writer.printf("%sCarVolumeGroup(%d)\n", indent, mId);
+            writer.printf("%sUserId(%d)\n", indent, mUserId);
+            writer.printf("%sGain values (min / max / default/ current): %d %d %d %d\n",
+                    indent, mMinGain, mMaxGain,
+                    mDefaultGain, getGainForIndexLocked(mCurrentGainIndex));
+            writer.printf("%sGain indexes (min / max / default / current): %d %d %d %d\n",
+                    indent, getMinGainIndex(), getMaxGainIndex(),
+                    getDefaultGainIndex(), mCurrentGainIndex);
+            for (int i = 0; i < mContextToAddress.size(); i++) {
+                writer.printf("%sContext: %s -> Address: %s\n", indent,
+                        CarAudioContext.toString(mContextToAddress.keyAt(i)),
+                        mContextToAddress.valueAt(i));
+            }
+            mAddressToCarAudioDeviceInfo.keySet().stream()
+                    .map(mAddressToCarAudioDeviceInfo::get)
+                    .forEach((info -> info.dump(indent, writer)));
 
-        // Empty line for comfortable reading
-        writer.println();
+            // Empty line for comfortable reading
+            writer.println();
+        }
     }
 
     /**
      * Load volumes for new user
      * @param userId new user to load
      */
-    void loadVolumesForUser(int userId) {
-        //Update the volume for the new user
-        updateUserId(userId);
-        //Update the current gain index
-        updateCurrentGainIndex();
-        //Reset devices with current gain index
+    void loadVolumesForUser(@UserIdInt int userId) {
+        synchronized (mLock) {
+            //Update the volume for the new user
+            updateUserIdLocked(userId);
+            //Update the current gain index
+            updateCurrentGainIndexLocked();
+            //Reset devices with current gain index
+        }
         setCurrentGainIndex(getCurrentGainIndex());
     }
 }
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/volume_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/volume_test.xml
index db48680..7cd5c95 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/volume_test.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/volume_test.xml
@@ -16,12 +16,25 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="horizontal" android:layout_width="match_parent"
     android:layout_height="match_parent">
-    <ListView
-        android:id="@+id/volume_list"
+
+    <LinearLayout
         android:layout_width="0dp"
         android:layout_height="match_parent"
+        android:layout_marginLeft="20dp"
+        android:orientation="vertical"
         android:layout_weight="3">
-    </ListView>
+        <com.google.android.material.tabs.TabLayout
+            android:id="@+id/zones_tab"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content">
+        </com.google.android.material.tabs.TabLayout>
+        <androidx.viewpager.widget.ViewPager
+            android:id="@+id/zone_view_pager"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1">
+        </androidx.viewpager.widget.ViewPager>
+    </LinearLayout>
 
     <LinearLayout
         android:layout_width="0dp"
@@ -31,12 +44,6 @@
         android:layout_weight="1">
 
         <Button
-            android:id="@+id/refresh"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/refresh_volume"/>
-
-        <Button
             android:id="@+id/volume_up"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/zone_volume_tab.xml b/tests/EmbeddedKitchenSinkApp/res/layout/zone_volume_tab.xml
new file mode 100644
index 0000000..bb38d5a
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/zone_volume_tab.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <ListView
+        android:id="@+id/volume_list"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="3">
+    </ListView>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/AudioZoneVolumeTabAdapter.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/AudioZoneVolumeTabAdapter.java
new file mode 100644
index 0000000..3e94f28
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/AudioZoneVolumeTabAdapter.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.car.kitchensink.volume;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentStatePagerAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class AudioZoneVolumeTabAdapter extends FragmentStatePagerAdapter {
+
+    private final List<Fragment> mFragmentList = new ArrayList<>();
+    private final List<String> mFragmentTitleList = new ArrayList<>();
+    AudioZoneVolumeTabAdapter(FragmentManager fm) {
+        super(fm);
+    }
+
+    @Override
+    public Fragment getItem(int position) {
+        return mFragmentList.get(position);
+    }
+
+    public void addFragment(Fragment fragment, String title) {
+        mFragmentList.add(fragment);
+        mFragmentTitleList.add(title);
+        notifyDataSetChanged();
+    }
+
+    @Nullable
+    @Override
+    public CharSequence getPageTitle(int position) {
+        return mFragmentTitleList.get(position);
+    }
+
+    @Override
+    public int getCount() {
+        return mFragmentList.size();
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeAdapter.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java
similarity index 82%
rename from tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeAdapter.java
rename to tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java
index ec071dc..eb0c111 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeAdapter.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java
@@ -25,22 +25,21 @@
 import android.widget.TextView;
 
 import com.google.android.car.kitchensink.R;
-import com.google.android.car.kitchensink.volume.VolumeTestFragment.VolumeInfo;
+import com.google.android.car.kitchensink.volume.VolumeTestFragment.CarAudioZoneVolumeInfo;
 
-
-public class VolumeAdapter extends ArrayAdapter<VolumeInfo> {
+public final class CarAudioZoneVolumeAdapter extends ArrayAdapter<CarAudioZoneVolumeInfo> {
 
     private final Context mContext;
-    private VolumeInfo[] mVolumeList;
+    private CarAudioZoneVolumeInfo[] mVolumeList;
     private final int mLayoutResourceId;
-    private VolumeTestFragment mFragment;
+    private CarAudioZoneVolumeFragment mFragment;
 
-
-    public VolumeAdapter(Context c, int layoutResourceId, VolumeInfo[] volumeList,
-            VolumeTestFragment fragment) {
-        super(c, layoutResourceId, volumeList);
+    public CarAudioZoneVolumeAdapter(Context context,
+            int layoutResourceId, CarAudioZoneVolumeInfo[] volumeList,
+            CarAudioZoneVolumeFragment fragment) {
+        super(context, layoutResourceId, volumeList);
         mFragment = fragment;
-        mContext = c;
+        mContext = context;
         this.mLayoutResourceId = layoutResourceId;
         this.mVolumeList = volumeList;
     }
@@ -95,18 +94,17 @@
         return mVolumeList.length;
     }
 
-
-    public void refreshVolumes(VolumeInfo[] volumes) {
+    public void refreshVolumes(CarAudioZoneVolumeInfo[] volumes) {
         mVolumeList = volumes;
         notifyDataSetChanged();
     }
 
-    static class ViewHolder {
-        TextView id;
-        TextView maxVolume;
-        TextView currentVolume;
-        Button upButton;
-        Button downButton;
-        Button requestButton;
+    private static final class ViewHolder {
+        public TextView id;
+        public TextView maxVolume;
+        public TextView currentVolume;
+        public Button upButton;
+        public Button downButton;
+        public Button requestButton;
     }
 }
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java
new file mode 100644
index 0000000..c1a712d
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.car.kitchensink.volume;
+
+import android.car.media.CarAudioManager;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import androidx.fragment.app.Fragment;
+
+import com.google.android.car.kitchensink.R;
+import com.google.android.car.kitchensink.volume.VolumeTestFragment.CarAudioZoneVolumeInfo;
+
+public final class CarAudioZoneVolumeFragment extends Fragment {
+    private static final String TAG = "CarVolumeTest."
+            + CarAudioZoneVolumeFragment.class.getSimpleName();
+    private static final boolean DEBUG = true;
+
+    private static final int MSG_VOLUME_CHANGED = 0;
+    private static final int MSG_REQUEST_FOCUS = 1;
+    private static final int MSG_FOCUS_CHANGED = 2;
+
+    private final int mZoneId;
+    private final CarAudioManager mCarAudioManager;
+    private final AudioManager mAudioManager;
+    private CarAudioZoneVolumeInfo[] mVolumeInfos =
+            new CarAudioZoneVolumeInfo[0];
+    private final Handler mHandler = new VolumeHandler();
+
+    private CarAudioZoneVolumeAdapter mCarAudioZoneVolumeAdapter;
+    private final SparseIntArray mGroupIdIndexMap = new SparseIntArray();
+
+    public void sendChangeMessage() {
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_VOLUME_CHANGED));
+    }
+
+    private class VolumeHandler extends Handler {
+        private AudioFocusListener mFocusListener;
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (DEBUG) {
+                Log.d(TAG, "zone " + mZoneId + " handleMessage : " + getMessageName(msg));
+            }
+            switch (msg.what) {
+                case MSG_VOLUME_CHANGED:
+                    initVolumeInfo();
+                    break;
+                case MSG_REQUEST_FOCUS:
+                    int groupId = msg.arg1;
+                    if (mFocusListener != null) {
+                        mAudioManager.abandonAudioFocus(mFocusListener);
+                        mVolumeInfos[mGroupIdIndexMap.get(groupId)].mHasFocus = false;
+                        mCarAudioZoneVolumeAdapter.notifyDataSetChanged();
+                    }
+
+                    mFocusListener = new AudioFocusListener(groupId);
+                    mAudioManager.requestAudioFocus(mFocusListener, groupId,
+                            AudioManager.AUDIOFOCUS_GAIN);
+                    break;
+                case MSG_FOCUS_CHANGED:
+                    int focusGroupId = msg.arg1;
+                    mVolumeInfos[mGroupIdIndexMap.get(focusGroupId)].mHasFocus = true;
+                    mCarAudioZoneVolumeAdapter.refreshVolumes(mVolumeInfos);
+                    break;
+                default :
+                    Log.wtf(TAG,"VolumeHandler handleMessage called with unknown message"
+                            + msg.what);
+
+            }
+        }
+    }
+
+    public CarAudioZoneVolumeFragment(int zoneId, CarAudioManager carAudioManager,
+            AudioManager audioManager) {
+        mZoneId = zoneId;
+        mCarAudioManager = carAudioManager;
+        mAudioManager = audioManager;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        if (DEBUG) {
+            Log.d(TAG, "onCreateView " + mZoneId);
+        }
+        View v = inflater.inflate(R.layout.zone_volume_tab, container, false);
+        ListView volumeListView = v.findViewById(R.id.volume_list);
+        mCarAudioZoneVolumeAdapter =
+                new CarAudioZoneVolumeAdapter(getContext(), R.layout.volume_item, mVolumeInfos,
+                        this);
+        initVolumeInfo();
+        volumeListView.setAdapter(mCarAudioZoneVolumeAdapter);
+        return v;
+    }
+
+    void initVolumeInfo() {
+        int volumeGroupCount = mCarAudioManager.getVolumeGroupCount(mZoneId);
+        mVolumeInfos = new CarAudioZoneVolumeInfo[volumeGroupCount + 1];
+        mGroupIdIndexMap.clear();
+        CarAudioZoneVolumeInfo titlesInfo = new CarAudioZoneVolumeInfo();
+        titlesInfo.mId = "Group id";
+        titlesInfo.mCurrent = "Current";
+        titlesInfo.mMax = "Max";
+        mVolumeInfos[0] = titlesInfo;
+
+        int i = 1;
+        for (int groupId = 0; groupId < volumeGroupCount; groupId++) {
+            CarAudioZoneVolumeInfo volumeInfo = new CarAudioZoneVolumeInfo();
+            mGroupIdIndexMap.put(groupId, i);
+            volumeInfo.mGroupId = groupId;
+            volumeInfo.mId = String.valueOf(groupId);
+            int current = mCarAudioManager.getGroupVolume(mZoneId, groupId);
+            int max = mCarAudioManager.getGroupMaxVolume(mZoneId, groupId);
+            volumeInfo.mCurrent = String.valueOf(current);
+            volumeInfo.mMax = String.valueOf(max);
+
+            mVolumeInfos[i] = volumeInfo;
+            if (DEBUG)
+            {
+                Log.d(TAG, groupId + " max: " + volumeInfo.mMax + " current: "
+                        + volumeInfo.mCurrent);
+            }
+            i++;
+        }
+        mCarAudioZoneVolumeAdapter.refreshVolumes(mVolumeInfos);
+    }
+
+    public void adjustVolumeByOne(int groupId, boolean up) {
+        if (mCarAudioManager == null) {
+            Log.e(TAG, "CarAudioManager is null");
+            return;
+        }
+        int current = mCarAudioManager.getGroupVolume(mZoneId, groupId);
+        int volume = current + (up ? 1 : -1);
+        mCarAudioManager.setGroupVolume(mZoneId, groupId, volume,
+                AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND);
+        if (DEBUG) {
+            Log.d(TAG, "Set group " + groupId + " volume " + volume + " in audio zone "
+                    + mZoneId);
+        }
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_VOLUME_CHANGED));
+    }
+
+    public void requestFocus(int groupId) {
+        // Automatic volume change only works for primary audio zone.
+        if (mZoneId == CarAudioManager.PRIMARY_AUDIO_ZONE) {
+            mHandler.sendMessage(mHandler.obtainMessage(MSG_REQUEST_FOCUS, groupId));
+        }
+    }
+
+    private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener {
+        private final int mGroupId;
+        AudioFocusListener(int groupId) {
+            mGroupId = groupId;
+        }
+        @Override
+        public void onAudioFocusChange(int focusChange) {
+            if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+                mHandler.sendMessage(mHandler.obtainMessage(MSG_FOCUS_CHANGED, mGroupId, 0));
+            } else {
+                Log.e(TAG, "Audio focus request failed");
+            }
+        }
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
index f753efe..5c662d4 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
@@ -18,32 +18,37 @@
 import android.car.Car;
 import android.car.Car.CarServiceLifecycleListener;
 import android.car.media.CarAudioManager;
+import android.car.media.CarAudioManager.CarVolumeCallback;
 import android.content.Context;
 import android.media.AudioManager;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
 import android.util.Log;
-import android.util.SparseIntArray;
+import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ListView;
 import android.widget.SeekBar;
 
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
+import androidx.viewpager.widget.ViewPager;
 
 import com.google.android.car.kitchensink.R;
+import com.google.android.material.tabs.TabLayout;
 
-public class VolumeTestFragment extends Fragment {
+import java.util.List;
+
+import javax.annotation.concurrent.GuardedBy;
+
+public final class VolumeTestFragment extends Fragment {
     private static final String TAG = "CarVolumeTest";
-    private static final int MSG_VOLUME_CHANGED = 0;
-    private static final int MSG_REQUEST_FOCUS = 1;
-    private static final int MSG_FOCUS_CHANGED= 2;
+    private static final boolean DEBUG = true;
 
     private AudioManager mAudioManager;
-    private VolumeAdapter mAdapter;
+    private AudioZoneVolumeTabAdapter mAudioZoneAdapter;
+    @GuardedBy("mLock")
+    private final SparseArray<CarAudioZoneVolumeFragment> mZoneVolumeFragments =
+            new SparseArray<>();
 
     private CarAudioManager mCarAudioManager;
     private Car mCar;
@@ -51,58 +56,10 @@
     private SeekBar mFader;
     private SeekBar mBalance;
 
-    private final Handler mHandler = new VolumeHandler();
+    private TabLayout mZonesTabLayout;
+    private Object mLock = new Object();
 
-    private class VolumeHandler extends Handler {
-        private AudioFocusListener mFocusListener;
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_VOLUME_CHANGED:
-                    initVolumeInfo();
-                    break;
-                case MSG_REQUEST_FOCUS:
-                    int groupId = msg.arg1;
-                    if (mFocusListener != null) {
-                        mAudioManager.abandonAudioFocus(mFocusListener);
-                        mVolumeInfos[mGroupIdIndexMap.get(groupId)].mHasFocus = false;
-                        mAdapter.notifyDataSetChanged();
-                    }
-
-                    mFocusListener = new AudioFocusListener(groupId);
-                    mAudioManager.requestAudioFocus(mFocusListener, groupId,
-                            AudioManager.AUDIOFOCUS_GAIN);
-                    break;
-                case MSG_FOCUS_CHANGED:
-                    int focusGroupId = msg.arg1;
-                    mVolumeInfos[mGroupIdIndexMap.get(focusGroupId)].mHasFocus = true;
-                    mAdapter.refreshVolumes(mVolumeInfos);
-                    break;
-
-            }
-        }
-    }
-
-    private VolumeInfo[] mVolumeInfos = new VolumeInfo[0];
-    private SparseIntArray mGroupIdIndexMap = new SparseIntArray();
-
-    private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener {
-        private final int mGroupId;
-        public AudioFocusListener(int groupId) {
-            mGroupId = groupId;
-        }
-        @Override
-        public void onAudioFocusChange(int focusChange) {
-            if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
-                mHandler.sendMessage(mHandler.obtainMessage(MSG_FOCUS_CHANGED, mGroupId, 0));
-            } else {
-                Log.e(TAG, "Audio focus request failed");
-            }
-        }
-    }
-
-    public static class VolumeInfo {
+    public static class CarAudioZoneVolumeInfo {
         public int mGroupId;
         public String mId;
         public String mMax;
@@ -110,30 +67,63 @@
         public boolean mHasFocus;
     }
 
+    private final class CarVolumeChangeListener extends CarVolumeCallback {
+        @Override
+        public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {
+            if (DEBUG) {
+                Log.d(TAG, "onGroupVolumeChanged volume changed for zone "
+                        + zoneId);
+            }
+            synchronized (mLock) {
+                CarAudioZoneVolumeFragment fragment = mZoneVolumeFragments.get(zoneId);
+                if (fragment != null) {
+                    fragment.sendChangeMessage();
+                }
+            }
+        }
+
+        @Override
+        public void onMasterMuteChanged(int zoneId, int flags) {
+            if (DEBUG) {
+                Log.d(TAG, "onMasterMuteChanged master mute "
+                        + mAudioManager.isMasterMute());
+            }
+        }
+    }
+
+    private final CarVolumeCallback mCarVolumeCallback = new CarVolumeChangeListener();
+
     private CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
         if (!ready) {
-            Log.d(TAG, "Disconnect from Car Service");
+            if (DEBUG) {
+                Log.d(TAG, "Disconnect from Car Service");
+            }
             return;
         }
-        Log.d(TAG, "Connected to Car Service");
+        if (DEBUG) {
+            Log.d(TAG, "Connected to Car Service");
+        }
         mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE);
         initVolumeInfo();
+        mCarAudioManager.registerCarVolumeCallback(mCarVolumeCallback);
     };
 
     @Override
     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                              @Nullable Bundle savedInstanceState) {
-        View v = inflater.inflate(R.layout.volume_test, container, false);
-
-        ListView volumeListView = v.findViewById(R.id.volume_list);
         mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
 
-        mAdapter = new VolumeAdapter(getContext(), R.layout.volume_item, mVolumeInfos, this);
-        volumeListView.setAdapter(mAdapter);
+        View v = inflater.inflate(R.layout.volume_test, container, false);
 
-        v.findViewById(R.id.refresh).setOnClickListener((view) -> initVolumeInfo());
+        mZonesTabLayout = v.findViewById(R.id.zones_tab);
+        ViewPager viewPager = (ViewPager) v.findViewById(R.id.zone_view_pager);
 
-        final SeekBar.OnSeekBarChangeListener seekListener = new SeekBar.OnSeekBarChangeListener() {
+        mAudioZoneAdapter = new AudioZoneVolumeTabAdapter(getChildFragmentManager());
+        viewPager.setAdapter(mAudioZoneAdapter);
+        mZonesTabLayout.setupWithViewPager(viewPager);
+
+        SeekBar.OnSeekBarChangeListener seekListener =
+                new SeekBar.OnSeekBarChangeListener() {
             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                 final float percent = (progress - 100) / 100.0f;
                 if (seekBar.getId() == R.id.fade_bar) {
@@ -159,48 +149,20 @@
         return v;
     }
 
-    public void adjustVolumeByOne(int groupId, boolean up) {
-        if (mCarAudioManager == null) {
-            Log.e(TAG, "CarAudioManager is null");
-            return;
-        }
-        int current = mCarAudioManager.getGroupVolume(groupId);
-        int volume = current + (up ? 1 : -1);
-        mCarAudioManager.setGroupVolume(groupId, volume,
-                AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_PLAY_SOUND);
-        Log.d(TAG, "Set group " + groupId + " volume " + volume);
-    }
-
-    public void requestFocus(int groupId) {
-        mHandler.sendMessage(mHandler.obtainMessage(MSG_REQUEST_FOCUS, groupId));
-    }
-
     private void initVolumeInfo() {
-        int volumeGroupCount = mCarAudioManager.getVolumeGroupCount();
-        mVolumeInfos = new VolumeInfo[volumeGroupCount + 1];
-        mGroupIdIndexMap.clear();
-        mVolumeInfos[0] = new VolumeInfo();
-        mVolumeInfos[0].mId = "Group id";
-        mVolumeInfos[0].mCurrent = "Current";
-        mVolumeInfos[0].mMax = "Max";
-
-        int i = 1;
-        for (int groupId = 0; groupId < volumeGroupCount; groupId++) {
-            mVolumeInfos[i] = new VolumeInfo();
-            mVolumeInfos[i].mGroupId = groupId;
-            mGroupIdIndexMap.put(groupId, i);
-            mVolumeInfos[i].mId = String.valueOf(groupId);
-
-
-            int current = mCarAudioManager.getGroupVolume(groupId);
-            int max = mCarAudioManager.getGroupMaxVolume(groupId);
-            mVolumeInfos[i].mCurrent = String.valueOf(current);
-            mVolumeInfos[i].mMax = String.valueOf(max);
-
-            Log.d(TAG, groupId + " max: " + mVolumeInfos[i].mMax + " current: "
-                    + mVolumeInfos[i].mCurrent);
-            i++;
+        synchronized (mLock) {
+            List<Integer> audioZoneIds = mCarAudioManager.getAudioZoneIds();
+            for (int index = 0; index < audioZoneIds.size(); index++) {
+                int zoneId = audioZoneIds.get(index);
+                CarAudioZoneVolumeFragment fragment =
+                        new CarAudioZoneVolumeFragment(zoneId, mCarAudioManager, mAudioManager);
+                mZonesTabLayout.addTab(mZonesTabLayout.newTab().setText("Audio Zone " + zoneId));
+                mAudioZoneAdapter.addFragment(fragment, "Audio Zone " + zoneId);
+                if (DEBUG) {
+                    Log.d(TAG, "Adding audio volume for zone " + zoneId);
+                }
+                mZoneVolumeFragments.put(zoneId, fragment);
+            }
         }
-        mAdapter.refreshVolumes(mVolumeInfos);
     }
 }
diff --git a/tests/carservice_test/src/com/android/car/audio/CarVolumeGroupTest.java b/tests/carservice_test/src/com/android/car/audio/CarVolumeGroupTest.java
index 58b7f71..cc9ecd3 100644
--- a/tests/carservice_test/src/com/android/car/audio/CarVolumeGroupTest.java
+++ b/tests/carservice_test/src/com/android/car/audio/CarVolumeGroupTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.car.audio;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -22,15 +24,18 @@
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.testng.Assert.expectThrows;
+
+import android.app.ActivityManager;
+import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.os.UserHandle;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.google.common.primitives.Ints;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 
@@ -40,22 +45,26 @@
 import java.util.Map;
 
 @RunWith(AndroidJUnit4.class)
-public class CarVolumeGroupTest {
+public class CarVolumeGroupTest extends AbstractExtendedMockitoTestCase{
     private static final int STEP_VALUE = 2;
     private static final int MIN_GAIN = 0;
     private static final int MAX_GAIN = 5;
     private static final int DEFAULT_GAIN = 0;
+    private static final int TEST_USER_10 = 10;
+    private static final int TEST_USER_11 = 11;
     private static final String OTHER_ADDRESS = "other_address";
     private static final String MEDIA_DEVICE_ADDRESS = "music";
     private static final String NAVIGATION_DEVICE_ADDRESS = "navigation";
 
-    @Rule
-    public final ExpectedException thrown = ExpectedException.none();
-
 
     private CarAudioDeviceInfo mMediaDevice;
     private CarAudioDeviceInfo mNavigationDevice;
 
+    @Override
+    protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
+        session.spyStatic(ActivityManager.class);
+    }
+
     @Before
     public void setUp() {
         mMediaDevice = generateCarAudioDeviceInfo(MEDIA_DEVICE_ADDRESS);
@@ -90,9 +99,10 @@
                 NAVIGATION_DEVICE_ADDRESS, STEP_VALUE + 1,
                 MIN_GAIN, MAX_GAIN);
 
-        thrown.expect(IllegalArgumentException.class);
-        thrown.expectMessage("Gain controls within one group must have same step value");
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, differentStepValueDevice);
+        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
+                () -> carVolumeGroup.bind(CarAudioContext.NAVIGATION, differentStepValueDevice));
+        assertThat(thrown).hasMessageThat()
+                .contains("Gain controls within one group must have same step value");
     }
 
     @Test
@@ -153,11 +163,10 @@
 
         carVolumeGroup.bind(CarAudioContext.NAVIGATION, mMediaDevice);
 
-        thrown.expect(IllegalArgumentException.class);
-        thrown.expectMessage(
-                "Context NAVIGATION has already been bound to " + MEDIA_DEVICE_ADDRESS);
-
-        carVolumeGroup.bind(CarAudioContext.NAVIGATION, mMediaDevice);
+        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
+                () -> carVolumeGroup.bind(CarAudioContext.NAVIGATION, mMediaDevice));
+        assertThat(thrown).hasMessageThat()
+                .contains("Context NAVIGATION has already been bound to " + MEDIA_DEVICE_ADDRESS);
     }
 
     @Test
@@ -241,20 +250,18 @@
     public void setCurrentGainIndex_checksNewGainIsAboveMin() {
         CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
 
-        thrown.expect(IllegalArgumentException.class);
-        thrown.expectMessage("Gain out of range (0:5) -2index -1");
-
-        carVolumeGroup.setCurrentGainIndex(-1);
+        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
+                () -> carVolumeGroup.setCurrentGainIndex(-1));
+        assertThat(thrown).hasMessageThat().contains("Gain out of range (0:5) -2index -1");
     }
 
     @Test
     public void setCurrentGainIndex_checksNewGainIsBelowMax() {
         CarVolumeGroup carVolumeGroup = testVolumeGroupSetup();
 
-        thrown.expect(IllegalArgumentException.class);
-        thrown.expectMessage("Gain out of range (0:5) 6index 3");
-
-        carVolumeGroup.setCurrentGainIndex(3);
+        IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
+                () -> carVolumeGroup.setCurrentGainIndex(3));
+        assertThat(thrown).hasMessageThat().contains("Gain out of range (0:5) 6index 3");
     }
 
     @Test
@@ -280,12 +287,12 @@
     public void loadVolumesForUser_setsCurrentGainIndexForUser() {
 
         List<Integer> users = new ArrayList<>();
-        users.add(10);
-        users.add(11);
+        users.add(TEST_USER_10);
+        users.add(TEST_USER_11);
 
         Map<Integer, Integer> storedGainIndex = new HashMap<>();
-        storedGainIndex.put(10, 2);
-        storedGainIndex.put(11, 0);
+        storedGainIndex.put(TEST_USER_10, 2);
+        storedGainIndex.put(TEST_USER_11, 0);
 
         CarAudioSettings settings =
                 generateCarAudioSettings(users, 0 , 0, storedGainIndex);
@@ -294,11 +301,11 @@
         CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
                 NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
         carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
-        carVolumeGroup.loadVolumesForUser(10);
+        carVolumeGroup.loadVolumesForUser(TEST_USER_10);
 
         assertEquals(2, carVolumeGroup.getCurrentGainIndex());
 
-        carVolumeGroup.loadVolumesForUser(11);
+        carVolumeGroup.loadVolumesForUser(TEST_USER_11);
 
         assertEquals(0, carVolumeGroup.getCurrentGainIndex());
     }
@@ -306,7 +313,7 @@
     @Test
     public void loadUserStoredGainIndex_setsCurrentGainIndexToDefault() {
         CarAudioSettings settings =
-                generateCarAudioSettings(0, 0 , 0, 10);
+                generateCarAudioSettings(TEST_USER_10, 0, 0, 10);
         CarVolumeGroup carVolumeGroup = new CarVolumeGroup(settings, 0, 0);
 
         CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
@@ -323,6 +330,50 @@
     }
 
     @Test
+    public void setCurrentGainIndex_setsCurrentGainIndexForUser() {
+        List<Integer> users = new ArrayList<>();
+        users.add(TEST_USER_11);
+
+        Map<Integer, Integer> storedGainIndex = new HashMap<>();
+        storedGainIndex.put(TEST_USER_11, 2);
+
+        CarAudioSettings settings =
+                generateCarAudioSettings(users, 0 , 0, storedGainIndex);
+        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(settings, 0, 0);
+
+        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
+                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
+        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
+        carVolumeGroup.loadVolumesForUser(TEST_USER_11);
+
+        carVolumeGroup.setCurrentGainIndex(MIN_GAIN);
+
+        verify(settings).storeVolumeGainIndexForUser(TEST_USER_11, 0, 0, MIN_GAIN);
+    }
+
+    @Test
+    public void setCurrentGainIndex_setsCurrentGainIndexForDefaultUser() {
+        List<Integer> users = new ArrayList<>();
+        users.add(UserHandle.USER_CURRENT);
+
+        Map<Integer, Integer> storedGainIndex = new HashMap<>();
+        storedGainIndex.put(UserHandle.USER_CURRENT, 2);
+
+        CarAudioSettings settings =
+                generateCarAudioSettings(users, 0 , 0, storedGainIndex);
+        CarVolumeGroup carVolumeGroup = new CarVolumeGroup(settings, 0, 0);
+
+        CarAudioDeviceInfo deviceInfo = generateCarAudioDeviceInfo(
+                NAVIGATION_DEVICE_ADDRESS, STEP_VALUE, MIN_GAIN, MAX_GAIN);
+        carVolumeGroup.bind(CarAudioContext.NAVIGATION, deviceInfo);
+
+        carVolumeGroup.setCurrentGainIndex(MIN_GAIN);
+
+        verify(settings)
+                .storeVolumeGainIndexForUser(UserHandle.USER_CURRENT, 0, 0, MIN_GAIN);
+    }
+
+    @Test
     public void bind_setsCurrentGainIndexToStoredGainIndex() {
         CarAudioSettings settings =
                 generateCarAudioSettings(0 , 0, 2);
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioSettingsUnitTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioSettingsUnitTest.java
index 89fd6ac..cf1b7fd 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioSettingsUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioSettingsUnitTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.car.media.CarAudioManager;
 import android.car.settings.CarSettings;
 import android.car.test.mocks.AbstractExtendedMockitoTestCase;
 import android.content.ContentResolver;
@@ -33,6 +34,10 @@
 public class CarAudioSettingsUnitTest extends AbstractExtendedMockitoTestCase {
 
     private static final int TEST_USER_ID_1 = 11;
+    private static final int TEST_ZONE_ID = CarAudioManager.PRIMARY_AUDIO_ZONE;
+    private static final int TEST_GROUP_ID = 0;
+    private static final int TEST_GAIN_INDEX = 10;
+    private static final String TEST_GAIN_INDEX_KEY = "android.car.VOLUME_GROUP/0";
 
 
     @Mock
@@ -61,6 +66,25 @@
                 .isTrue();
     }
 
+    @Test
+    public void getStoredVolumeGainIndexForUser_returnsSavedValue() {
+        setStoredVolumeGainIndexForUser(TEST_GAIN_INDEX);
+
+        assertThat(mCarAudioSettings.getStoredVolumeGainIndexForUser(TEST_USER_ID_1, TEST_ZONE_ID,
+                        TEST_GROUP_ID)).isEqualTo(TEST_GAIN_INDEX);
+    }
+
+    @Test
+    public void storedVolumeGainIndexForUser_savesValue() {
+        mCarAudioSettings.storeVolumeGainIndexForUser(TEST_USER_ID_1, TEST_ZONE_ID,
+                TEST_GROUP_ID, TEST_GAIN_INDEX);
+        assertThat(getSettingsInt(TEST_GAIN_INDEX_KEY)).isEqualTo(TEST_GAIN_INDEX);
+    }
+
+    private void setStoredVolumeGainIndexForUser(int gainIndexForUser) {
+        putSettingsInt(TEST_GAIN_INDEX_KEY, gainIndexForUser);
+    }
+
     private void setRejectNavigationOnCallSettingsValues(int settingsValue) {
         putSettingsInt(CarSettings.Secure.KEY_AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL,
                 settingsValue);