Merge "Specify version for aidl_interface explicitly"
diff --git a/hostsidetests/hdmicec/app/src/android/hdmicec/app/HdmiCecAudioManager.java b/hostsidetests/hdmicec/app/src/android/hdmicec/app/HdmiCecAudioManager.java
index f04490f..8167e5f 100644
--- a/hostsidetests/hdmicec/app/src/android/hdmicec/app/HdmiCecAudioManager.java
+++ b/hostsidetests/hdmicec/app/src/android/hdmicec/app/HdmiCecAudioManager.java
@@ -30,21 +30,22 @@
  * A simple app that can be used to mute, unmute, set volume or get the volume status of a device.
  * The actions supported are:
  *
- * 1. android.hdmicec.app.MUTE: Mutes the STREAM_MUSIC of the device,
- *                              irrespective of the previous state.
- *    Usage: START_COMMAND -a android.hdmicec.app.MUTE
- * 2. android.hdmicec.app.UNMUTE: Unmutes the STREAM_MUSIC of the device,
- *                                irrespective of the previous state.
- *    Usage: START_COMMAND -a android.hdmicec.app.UNMUTE
- * 3. android.hdmicec.app.REPORT_VOLUME: Reports if the STREAM_MUSIC of the device is muted and
- *                                       if not muted, the current volume level in percent.
- *    Usage: START_COMMAND -a android.hdmicec.app.REPORT_VOLUME
- * 4. android.hdmicec.app.SET_VOLUME: Sets the volume of STREAM_MUSIC to a particular level.
- *                                    Has to be used with --ei "volumePercent" x.
- *    Usage: START_COMMAND -a android.hdmicec.app.SET_VOLUME --ei "volumePercent" x
+ * <p>1. android.hdmicec.app.MUTE: Mutes the STREAM_MUSIC of the device, irrespective of the
+ * previous state. Usage: START_COMMAND -a android.hdmicec.app.MUTE
  *
- * where START_COMMAND is
- * adb shell am start -n "android.hdmicec.app/android.hdmicec.app.HdmiCecAudioManager"
+ * <p>2. android.hdmicec.app.UNMUTE: Unmutes the STREAM_MUSIC of the device, irrespective of the
+ * previous state. Usage: START_COMMAND -a android.hdmicec.app.UNMUTE
+ *
+ * <p>3. android.hdmicec.app.REPORT_VOLUME: Reports current volume level in percent if not muted.
+ * Adds 128 to volume percent level if the device is muted. Usage: START_COMMAND -a
+ * android.hdmicec.app.REPORT_VOLUME
+ *
+ * <p>4. android.hdmicec.app.SET_VOLUME: Sets the volume of STREAM_MUSIC to a particular level. Has
+ * to be used with --ei "volumePercent" x. Usage: START_COMMAND -a android.hdmicec.app.SET_VOLUME
+ * --ei "volumePercent" x
+ *
+ * <p>where START_COMMAND is adb shell am start -n
+ * "android.hdmicec.app/android.hdmicec.app.HdmiCecAudioManager"
  */
 public class HdmiCecAudioManager extends Activity {
 
@@ -54,7 +55,8 @@
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
         AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
-
+        int minVolume = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
+        int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
         switch(getIntent().getAction()) {
             case "android.hdmicec.app.MUTE":
                 audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
@@ -65,20 +67,15 @@
                         AudioManager.ADJUST_UNMUTE, 0);
                 break;
             case "android.hdmicec.app.REPORT_VOLUME":
+                int volumeLevel = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+                int percentageVolume = 100 * volumeLevel / (maxVolume - minVolume);
                 if (audioManager.isStreamMute(AudioManager.STREAM_MUSIC)) {
-                    Log.i(TAG, "Device muted.");
-                } else {
-                    int minVolume = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
-                    int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
-                    int volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
-                    int percentVolume = 100 * volume / (maxVolume - minVolume);
-                    Log.i(TAG, "Volume at " + percentVolume + "%");
+                    percentageVolume += 128;
                 }
+                Log.i(TAG, "Volume at " + percentageVolume + "%");
                 break;
             case "android.hdmicec.app.SET_VOLUME":
                 int percentVolume = getIntent().getIntExtra("volumePercent", 50);
-                int minVolume = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
-                int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
                 int volume = minVolume + ((maxVolume - minVolume) * percentVolume / 100);
                 audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
                 Log.i(TAG, "Set volume to " + volume + " (" + percentVolume + "%)");
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/AudioManagerHelper.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/AudioManagerHelper.java
new file mode 100644
index 0000000..88091c6
--- /dev/null
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/AudioManagerHelper.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hdmicec.cts;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import com.android.tradefed.device.ITestDevice;
+
+/** Helper class to get DUT audio status using Audio manager app */
+public final class AudioManagerHelper {
+
+    /** The package name of the APK. */
+    private static final String PACKAGE = "android.hdmicec.app";
+
+    /** The class name of the main activity in the APK. */
+    private static final String CLASS = "HdmiCecAudioManager";
+
+    /** The command to launch the main activity. */
+    private static final String START_COMMAND =
+            String.format("am start -n %s/%s.%s -a ", PACKAGE, PACKAGE, CLASS);
+
+    /** The command to clear the main activity. */
+    private static final String CLEAR_COMMAND = String.format("pm clear %s", PACKAGE);
+
+    public static final int MAX_AUDIO_FORMATS = 4;
+    public static final int MAX_VALID_AUDIO_FORMATS = 2;
+
+    public static List<Integer> mSupportedAudioFormats = null;
+
+    public static void muteDevice(ITestDevice device, HdmiCecClientWrapper hdmiCecClient)
+            throws Exception {
+        // Clear activity
+        device.executeShellCommand(CLEAR_COMMAND);
+        // Clear logcat.
+        device.executeAdbCommand("logcat", "-c");
+        // Start the APK and wait for it to complete.
+        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.MUTE");
+        // The audio device should send <Report Audio Status> message after mute.
+        hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.REPORT_AUDIO_STATUS);
+    }
+
+    public static void unmuteDevice(ITestDevice device, HdmiCecClientWrapper hdmiCecClient)
+            throws Exception {
+        // Clear activity
+        device.executeShellCommand(CLEAR_COMMAND);
+        // Start the APK and wait for it to complete.
+        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.UNMUTE");
+        // The audio device should send <Report Audio Status> message after unmute.
+        hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.REPORT_AUDIO_STATUS);
+    }
+
+    public static boolean isDeviceMuted(ITestDevice device) throws Exception {
+        // Clear activity
+        device.executeShellCommand(CLEAR_COMMAND);
+        // Clear logcat.
+        device.executeAdbCommand("logcat", "-c");
+        // Start the APK and wait for it to complete.
+        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.REPORT_VOLUME");
+        return (LogHelper.parseDutVolume(device, CLASS) >= 128);
+    }
+
+    public static void setDeviceVolume(ITestDevice device, int percentVolume) throws Exception {
+        // Clear activity
+        device.executeShellCommand(CLEAR_COMMAND);
+        // Start the APK and wait for it to complete.
+        device.executeShellCommand(
+                START_COMMAND
+                        + "android.hdmicec.app.SET_VOLUME --ei "
+                        + "\"volumePercent\" "
+                        + percentVolume);
+    }
+
+    public static int getDutAudioVolume(ITestDevice device) throws Exception {
+        // Clear activity
+        device.executeShellCommand(CLEAR_COMMAND);
+        // Clear logcat.
+        device.executeAdbCommand("logcat", "-c");
+        // Start the APK and wait for it to complete.
+        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.REPORT_VOLUME");
+        return LogHelper.parseDutVolume(device, CLASS);
+    }
+
+    public static String getRequestSadFormatsParams(ITestDevice device, boolean sendValidFormats)
+            throws Exception {
+        // Clear activity
+        device.executeShellCommand(CLEAR_COMMAND);
+        // Clear logcat.
+        device.executeAdbCommand("logcat", "-c");
+        // Start the APK and wait for it to complete.
+        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.GET_SUPPORTED_SAD_FORMATS");
+        mSupportedAudioFormats = LogHelper.getSupportedAudioFormats(device);
+
+        // Create a list of all the audio format codes according to CEA-861-D. Remove the supported
+        // audio format codes from it, to get the unsupported audio format codes.
+        List<Integer> mAllCodecFormats =
+                IntStream.range(1, 15).boxed().collect(Collectors.toList());
+        List<Integer> unsupportedAudioFormats = new ArrayList<>();
+        unsupportedAudioFormats.addAll(mAllCodecFormats);
+        unsupportedAudioFormats.removeAll(mSupportedAudioFormats);
+        // Create params message for REQUEST_SHORT_AUDIO_DESCRIPTOR
+        String messageParams = "";
+        int i = 0;
+        int listIndex = 0;
+        if (sendValidFormats) {
+            while (i < Math.min(MAX_VALID_AUDIO_FORMATS, mSupportedAudioFormats.size())) {
+                messageParams += CecMessage.formatParams(mSupportedAudioFormats.get(listIndex), 2);
+                i++;
+                listIndex++;
+            }
+            listIndex = 0;
+        }
+        while (i < Math.min(MAX_AUDIO_FORMATS, unsupportedAudioFormats.size())) {
+            messageParams += CecMessage.formatParams(unsupportedAudioFormats.get(listIndex), 2);
+            i++;
+            listIndex++;
+        }
+        return messageParams;
+    }
+}
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java
index 025f2d1..4605311 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/BaseHdmiCecCtsTest.java
@@ -92,6 +92,14 @@
                         HdmiCecConstants.HDMI_DEVICE_TYPE_PROPERTY,
                         dutLogicalAddress.getDeviceTypeString());
         }
+
+        /** This rule will skip the test if the DUT belongs to the HDMI device type deviceType. */
+        public static TestRule skipDeviceType(BaseHostJUnit4Test testPointer, int deviceType) {
+            return RequiredPropertyRule.asCsvDoesNotContainsValue(
+                    testPointer,
+                    HdmiCecConstants.HDMI_DEVICE_TYPE_PROPERTY,
+                    Integer.toString(deviceType));
+        }
     }
 
     @Option(name = HdmiCecConstants.PHYSICAL_ADDRESS_NAME,
@@ -138,4 +146,39 @@
         }
         throw new Exception("Could not parse logical address from dumpsys.");
     }
+
+    /**
+     * Parses the dumpsys hdmi_control to get the logical address of the current device registered
+     * as active source.
+     */
+    public LogicalAddress getDumpsysActiveSourceLogicalAddress() throws Exception {
+        String line;
+        String pattern =
+                "(.*?)"
+                        + "(mActiveSource: )"
+                        + "(\\(0x)"
+                        + "(?<logicalAddress>\\d+)"
+                        + "(, )"
+                        + "(0x)"
+                        + "(?<physicalAddress>\\d+)"
+                        + "(\\))"
+                        + "(.*?)";
+        Pattern p = Pattern.compile(pattern);
+        Matcher m;
+        ITestDevice device = getDevice();
+        String dumpsys = device.executeShellCommand("dumpsys hdmi_control");
+        BufferedReader reader = new BufferedReader(new StringReader(dumpsys));
+        while ((line = reader.readLine()) != null) {
+            m = p.matcher(line);
+            if (m.matches()) {
+                try {
+                    int address = Integer.decode(m.group("logicalAddress"));
+                    return LogicalAddress.getLogicalAddress(address);
+                } catch (NumberFormatException ne) {
+                    throw new Exception("Could not correctly parse the logical address");
+                }
+            }
+        }
+        throw new Exception("Could not parse active source from dumpsys.");
+    }
 }
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/CecOperand.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/CecOperand.java
index 5aa547e..f04a1a7 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/CecOperand.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/CecOperand.java
@@ -21,6 +21,7 @@
 
 public enum CecOperand {
     FEATURE_ABORT(0x00),
+    IMAGE_VIEW_ON(0x04),
     TEXT_VIEW_ON(0x0d),
     SET_MENU_LANGUAGE(0x32),
     STANDBY(0x36),
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecClientWrapper.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecClientWrapper.java
index b926001..624559d 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecClientWrapper.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/HdmiCecClientWrapper.java
@@ -49,6 +49,7 @@
     private LogicalAddress targetDevice = LogicalAddress.UNKNOWN;
     private String clientParams[];
     private StringBuilder sendVendorCommand = new StringBuilder("cmd hdmi_control vendorcommand ");
+    private int physicalAddress = 0xFFFF;
 
     public HdmiCecClientWrapper(String ...clientParams) {
         this.clientParams = clientParams;
@@ -159,12 +160,16 @@
          * address 2.0.0.0 */
         commands.add("-p");
         commands.add("2");
+        physicalAddress = 0x2000;
         if (startAsTv) {
             commands.add("-t");
             commands.add("x");
             selfDevice = LogicalAddress.TV;
         }
         commands.addAll(Arrays.asList(clientParams));
+        if (Arrays.asList(clientParams).contains("a")) {
+            selfDevice = LogicalAddress.AUDIO_SYSTEM;
+        }
 
         List<String> comPorts = getValidCecClientPorts();
 
@@ -211,6 +216,66 @@
     }
 
     /**
+     * Broadcasts a CEC ACTIVE_SOURCE message from client device source through the output console
+     * of the cec-communication channel.
+     */
+    public void broadcastActiveSource(LogicalAddress source) throws Exception {
+        int sourcePa = (source == selfDevice) ? physicalAddress : 0xFFFF;
+        sendCecMessage(
+                source,
+                LogicalAddress.BROADCAST,
+                CecOperand.ACTIVE_SOURCE,
+                CecMessage.formatParams(sourcePa, HdmiCecConstants.PHYSICAL_ADDRESS_LENGTH));
+    }
+
+    /**
+     * Broadcasts a CEC ACTIVE_SOURCE message with physicalAddressOfActiveDevice from client device
+     * source through the output console of the cec-communication channel.
+     */
+    public void broadcastActiveSource(LogicalAddress source, int physicalAddressOfActiveDevice)
+            throws Exception {
+        sendCecMessage(
+                source,
+                LogicalAddress.BROADCAST,
+                CecOperand.ACTIVE_SOURCE,
+                CecMessage.formatParams(
+                        physicalAddressOfActiveDevice, HdmiCecConstants.PHYSICAL_ADDRESS_LENGTH));
+    }
+
+    /**
+     * Broadcasts a CEC REPORT_PHYSICAL_ADDRESS message from client device source through the output
+     * console of the cec-communication channel.
+     */
+    public void broadcastReportPhysicalAddress(LogicalAddress source) throws Exception {
+        String deviceType = CecMessage.formatParams(source.getDeviceType());
+        int sourcePa = (source == selfDevice) ? physicalAddress : 0xFFFF;
+        String physicalAddress =
+                CecMessage.formatParams(sourcePa, HdmiCecConstants.PHYSICAL_ADDRESS_LENGTH);
+        sendCecMessage(
+                source,
+                LogicalAddress.BROADCAST,
+                CecOperand.REPORT_PHYSICAL_ADDRESS,
+                physicalAddress + deviceType);
+    }
+
+    /**
+     * Broadcasts a CEC REPORT_PHYSICAL_ADDRESS message with physicalAddressToReport from client
+     * device source through the output console of the cec-communication channel.
+     */
+    public void broadcastReportPhysicalAddress(LogicalAddress source, int physicalAddressToReport)
+            throws Exception {
+        String deviceType = CecMessage.formatParams(source.getDeviceType());
+        String physicalAddress =
+                CecMessage.formatParams(
+                        physicalAddressToReport, HdmiCecConstants.PHYSICAL_ADDRESS_LENGTH);
+        sendCecMessage(
+                source,
+                LogicalAddress.BROADCAST,
+                CecOperand.REPORT_PHYSICAL_ADDRESS,
+                physicalAddress + deviceType);
+    }
+
+    /**
      * Sends a CEC message from source device to a destination device through the output console of
      * the cec-communication channel with the appended params.
      */
@@ -442,6 +507,21 @@
         return selfDevice;
     }
 
+    /** Set the physical address of the cec-client instance */
+    public void setPhysicalAddress(int newPhysicalAddress) throws Exception {
+        String command =
+                String.format(
+                        "pa %02d %02d",
+                        (newPhysicalAddress & 0xFF00) >> 8, newPhysicalAddress & 0xFF);
+        sendConsoleMessage(command);
+        physicalAddress = newPhysicalAddress;
+    }
+
+    /** Get the physical address of the cec-client instance, will return 0xFFFF if uninitialised */
+    public int getPhysicalAddress() {
+        return physicalAddress;
+    }
+
     /**
      * Kills the cec-client process that was created in init().
      */
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/LogHelper.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/LogHelper.java
index a7692f38..7b2c857 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/LogHelper.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/LogHelper.java
@@ -17,6 +17,8 @@
 package android.hdmicec.cts;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tradefed.device.ITestDevice;
@@ -64,6 +66,21 @@
         assertThat(testString).isIn(expectedOutputs);
     }
 
+    /** This method will return the DUT volume. */
+    public static int parseDutVolume(ITestDevice device, String tag) throws Exception {
+        String testString = getLog(device, tag);
+        assertWithMessage("No log from Audio Manager which reports the DUT volume percentage")
+                .that(testString)
+                .isNotEqualTo("");
+        try {
+            String volume = testString.split("at")[1].trim().replaceAll("%", "");
+            return Integer.parseInt(volume);
+        } catch (NumberFormatException e) {
+            throw new NumberFormatException(
+                    "Volume obtained from Audio Manager can" + "not be parsed");
+        }
+    }
+
     public static void assertLogDoesNotContain(ITestDevice device, String tag,
                                                String expectedOutput) throws Exception {
         String testString = getLog(device, tag);
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/RequiredPropertyRule.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/RequiredPropertyRule.java
index f3998adb..8d44bef 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/RequiredPropertyRule.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/RequiredPropertyRule.java
@@ -16,6 +16,7 @@
 
 package android.hdmicec.cts;
 
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tradefed.device.ITestDevice;
@@ -99,6 +100,34 @@
             }
         };
     }
+
+    public static RequiredPropertyRule asCsvDoesNotContainsValue(
+            final BaseHostJUnit4Test test, final String propertyName, final String propertyValue) {
+        return new RequiredPropertyRule() {
+            @Override
+            public Statement apply(final Statement base, final Description description) {
+                return new Statement() {
+                    @Override
+                    public void evaluate() throws Throwable {
+                        List<String> deviceProperties =
+                                Arrays.asList(
+                                        getDevicePropertyValue(test, propertyName)
+                                                .replaceAll("\\s+", "")
+                                                .split(","));
+                        assumeFalse(
+                                "The Property "
+                                        + propertyName
+                                        + " = "
+                                        + propertyValue
+                                        + " is expected to not be present in "
+                                        + deviceProperties.toString()
+                                        + " of device "
+                                        + test.getDevice().getSerialNumber(),
+                                deviceProperties.contains(propertyValue));
+                        base.evaluate();
+                    }
+                };
+            }
+        };
+    }
 }
-
-
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecSystemAudioModeTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecSystemAudioModeTest.java
index b284215..391916f 100644
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecSystemAudioModeTest.java
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/audio/HdmiCecSystemAudioModeTest.java
@@ -21,17 +21,13 @@
 
 import com.google.common.collect.Range;
 
+import android.hdmicec.cts.AudioManagerHelper;
 import android.hdmicec.cts.BaseHdmiCecCtsTest;
 import android.hdmicec.cts.CecMessage;
 import android.hdmicec.cts.CecOperand;
-import android.hdmicec.cts.HdmiCecClientWrapper;
 import android.hdmicec.cts.HdmiCecConstants;
-import android.hdmicec.cts.LogHelper;
 import android.hdmicec.cts.LogicalAddress;
-import android.hdmicec.cts.RequiredPropertyRule;
-import android.hdmicec.cts.RequiredFeatureRule;
 
-import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 
 import org.junit.After;
@@ -41,37 +37,16 @@
 import org.junit.runner.RunWith;
 import org.junit.Test;
 
-import java.util.ArrayList;
 import java.util.concurrent.TimeUnit;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 
 /** HDMI CEC test to test system audio mode (Section 11.2.15) */
 @Ignore("b/162820841")
 @RunWith(DeviceJUnit4ClassRunner.class)
 public final class HdmiCecSystemAudioModeTest extends BaseHdmiCecCtsTest {
 
-    /** The package name of the APK. */
-    private static final String PACKAGE = "android.hdmicec.app";
-
-    /** The class name of the main activity in the APK. */
-    private static final String CLASS = "HdmiCecAudioManager";
-
-    /** The command to launch the main activity. */
-    private static final String START_COMMAND = String.format(
-            "am start -n %s/%s.%s -a ", PACKAGE, PACKAGE, CLASS);
-
-    /** The command to clear the main activity. */
-    private static final String CLEAR_COMMAND = String.format("pm clear %s", PACKAGE);
-
     private static final LogicalAddress AUDIO_DEVICE = LogicalAddress.AUDIO_SYSTEM;
     private static final int ON = 0x1;
     private static final int OFF = 0x0;
-    private static final int MAX_AUDIO_FORMATS = 4;
-    private static final int MAX_VALID_AUDIO_FORMATS = 2;
-
-    private List<Integer> mSupportedAudioFormats = null;
 
     public HdmiCecSystemAudioModeTest() {
         super(AUDIO_DEVICE, "-t", "t");
@@ -85,91 +60,6 @@
             .around(CecRules.requiresDeviceType(this, AUDIO_DEVICE))
             .around(hdmiCecClient);
 
-    private String getRequestSadFormatsParams(boolean sendValidFormats) throws Exception {
-        ITestDevice device = getDevice();
-        // Clear activity
-        device.executeShellCommand(CLEAR_COMMAND);
-        // Clear logcat.
-        device.executeAdbCommand("logcat", "-c");
-        // Start the APK and wait for it to complete.
-        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.GET_SUPPORTED_SAD_FORMATS");
-        mSupportedAudioFormats = LogHelper.getSupportedAudioFormats(getDevice());
-
-        // Create a list of all the audio format codes according to CEA-861-D. Remove the supported
-        // audio format codes from it, to get the unsupported audio format codes.
-        List<Integer> mAllCodecFormats =
-                IntStream.range(1, 15).boxed().collect(Collectors.toList());
-        List<Integer> unsupportedAudioFormats = new ArrayList<>();
-        unsupportedAudioFormats.addAll(mAllCodecFormats);
-        unsupportedAudioFormats.removeAll(mSupportedAudioFormats);
-        // Create params message for REQUEST_SHORT_AUDIO_DESCRIPTOR
-        String messageParams = "";
-        int i = 0;
-        int listIndex = 0;
-        if (sendValidFormats) {
-            while (i < Math.min(MAX_VALID_AUDIO_FORMATS, mSupportedAudioFormats.size())) {
-                messageParams +=
-                        CecMessage.formatParams(mSupportedAudioFormats.get(listIndex), 2);
-                i++;
-                listIndex++;
-            }
-            listIndex = 0;
-        }
-        while (i < Math.min(MAX_AUDIO_FORMATS, unsupportedAudioFormats.size())) {
-            messageParams += CecMessage.formatParams(unsupportedAudioFormats.get(listIndex), 2);
-            i++;
-            listIndex++;
-        }
-        return messageParams;
-    }
-
-    private void muteDevice() throws Exception {
-        ITestDevice device = getDevice();
-        // Clear activity
-        device.executeShellCommand(CLEAR_COMMAND);
-        // Clear logcat.
-        device.executeAdbCommand("logcat", "-c");
-        // Start the APK and wait for it to complete.
-        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.MUTE");
-        // The audio device should send <Report Audio Status> message after mute.
-        hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.REPORT_AUDIO_STATUS);
-    }
-
-    private void unmuteDevice() throws Exception {
-        ITestDevice device = getDevice();
-        // Clear activity
-        device.executeShellCommand(CLEAR_COMMAND);
-        // Start the APK and wait for it to complete.
-        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.UNMUTE");
-        // The audio device should send <Report Audio Status> message after unmute.
-        hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.REPORT_AUDIO_STATUS);
-    }
-
-    public boolean isDeviceMuted() throws Exception {
-        ITestDevice device = getDevice();
-        // Clear activity
-        device.executeShellCommand(CLEAR_COMMAND);
-        // Clear logcat.
-        device.executeAdbCommand("logcat", "-c");
-        // Start the APK and wait for it to complete.
-        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.REPORT_VOLUME");
-        try {
-            LogHelper.assertLog(getDevice(), CLASS, "Device muted.");
-            return true;
-        } catch(Exception e) {
-            return false;
-        }
-    }
-
-    public void setDeviceVolume(int percentVolume) throws Exception {
-        ITestDevice device = getDevice();
-        // Clear activity
-        device.executeShellCommand(CLEAR_COMMAND);
-        // Start the APK and wait for it to complete.
-        device.executeShellCommand(START_COMMAND + "android.hdmicec.app.SET_VOLUME --ei " +
-                "\"volumePercent\" " + percentVolume);
-    }
-
     public void sendSystemAudioModeTermination() throws Exception {
         hdmiCecClient.sendCecMessage(LogicalAddress.TV, AUDIO_DEVICE,
                 CecOperand.SYSTEM_AUDIO_MODE_REQUEST);
@@ -222,7 +112,7 @@
 
     @After
     public void resetVolume() throws Exception {
-        setDeviceVolume(20);
+        AudioManagerHelper.setDeviceVolume(getDevice(), 20);
     }
 
     /**
@@ -354,14 +244,16 @@
      */
     @Test
     public void cect_11_2_15_8_HandleUcpMute() throws Exception {
-        unmuteDevice();
+        AudioManagerHelper.unmuteDevice(getDevice(), hdmiCecClient);
         hdmiCecClient.sendCecMessage(LogicalAddress.TV, AUDIO_DEVICE,
                 CecOperand.SYSTEM_AUDIO_MODE_REQUEST,
                 CecMessage.formatParams(HdmiCecConstants.TV_PHYSICAL_ADDRESS,
                     HdmiCecConstants.PHYSICAL_ADDRESS_LENGTH));
         hdmiCecClient.sendUserControlPressAndRelease(LogicalAddress.TV, AUDIO_DEVICE,
                 HdmiCecConstants.CEC_CONTROL_MUTE, false);
-        assertWithMessage("Device is not muted").that(isDeviceMuted()).isTrue();
+        assertWithMessage("Device is not muted")
+                .that(AudioManagerHelper.isDeviceMuted(getDevice()))
+                .isTrue();
     }
 
     /**
@@ -372,8 +264,8 @@
     @Test
     public void cect_11_2_15_9_ReportAudioStatus_0() throws Exception {
         sendSystemAudioModeInitiation();
-        unmuteDevice();
-        setDeviceVolume(0);
+        AudioManagerHelper.unmuteDevice(getDevice(), hdmiCecClient);
+        AudioManagerHelper.setDeviceVolume(getDevice(), 0);
         int reportedVolume = getDutAudioStatus();
         assertThat(reportedVolume).isAnyOf(0, 128);
     }
@@ -386,8 +278,8 @@
     @Test
     public void cect_11_2_15_9_ReportAudioStatus_50_unmuted() throws Exception {
         sendSystemAudioModeInitiation();
-        unmuteDevice();
-        setDeviceVolume(50);
+        AudioManagerHelper.unmuteDevice(getDevice(), hdmiCecClient);
+        AudioManagerHelper.setDeviceVolume(getDevice(), 50);
         int reportedVolume = getDutAudioStatus();
         /* Allow for a range of volume, since the actual volume set will depend on the device's
         volume resolution. */
@@ -402,8 +294,8 @@
     @Test
     public void cect_11_2_15_9_ReportAudioStatus_100_unmuted() throws Exception {
         sendSystemAudioModeInitiation();
-        unmuteDevice();
-        setDeviceVolume(100);
+        AudioManagerHelper.unmuteDevice(getDevice(), hdmiCecClient);
+        AudioManagerHelper.setDeviceVolume(getDevice(), 100);
         int reportedVolume = getDutAudioStatus();
         assertThat(reportedVolume).isEqualTo(100);
     }
@@ -416,7 +308,7 @@
     @Test
     public void cect_11_2_15_9_ReportAudioStatusMuted() throws Exception {
         sendSystemAudioModeInitiation();
-        muteDevice();
+        AudioManagerHelper.muteDevice(getDevice(), hdmiCecClient);
         int reportedVolume = getDutAudioStatus();
         /* If device is muted, the 8th bit of CEC message parameters is set and the volume will
         be greater than 127. */
@@ -430,21 +322,29 @@
      */
     @Test
     public void cect_11_2_15_13_ValidShortAudioDescriptor() throws Exception {
-        hdmiCecClient.sendCecMessage(LogicalAddress.TV, AUDIO_DEVICE,
-                CecOperand.REQUEST_SHORT_AUDIO_DESCRIPTOR, getRequestSadFormatsParams(true));
+        hdmiCecClient.sendCecMessage(
+                LogicalAddress.TV,
+                AUDIO_DEVICE,
+                CecOperand.REQUEST_SHORT_AUDIO_DESCRIPTOR,
+                AudioManagerHelper.getRequestSadFormatsParams(getDevice(), true));
         String message = hdmiCecClient.checkExpectedOutput(LogicalAddress.TV,
                 CecOperand.REPORT_SHORT_AUDIO_DESCRIPTOR);
+        int numFormats =
+                Math.min(
+                        AudioManagerHelper.mSupportedAudioFormats.size(),
+                        AudioManagerHelper.MAX_VALID_AUDIO_FORMATS);
         /* Each Short Audio Descriptor is 3 bytes long. In the first byte of the params, bits 3-6
          * will have the audio format. Bit 7 will always 0 for audio format defined in CEA-861-D.
          * Bits 0-2 represent (Max number of channels - 1). Discard bits 0-2 and check for the
          * format.
          * Iterate the params by 3 bytes(6 nibbles) and extract only the first byte(2 nibbles).
          */
-        for (int i = 0; i < Math.min(mSupportedAudioFormats.size(), MAX_VALID_AUDIO_FORMATS); i++) {
+        for (int i = 0; i < numFormats; i++) {
             int audioFormat =
                     CecMessage.getParams(message, 6 * i, 6 * i + 2) >>> 3;
             assertWithMessage("Could not find audio format " + audioFormat)
-                    .that(mSupportedAudioFormats).contains(audioFormat);
+                    .that(AudioManagerHelper.mSupportedAudioFormats)
+                    .contains(audioFormat);
         }
     }
 
@@ -456,8 +356,11 @@
      */
     @Test
     public void cect_11_2_15_14_InvalidShortAudioDescriptor() throws Exception {
-        hdmiCecClient.sendCecMessage(LogicalAddress.TV, AUDIO_DEVICE,
-                CecOperand.REQUEST_SHORT_AUDIO_DESCRIPTOR, getRequestSadFormatsParams(false));
+        hdmiCecClient.sendCecMessage(
+                LogicalAddress.TV,
+                AUDIO_DEVICE,
+                CecOperand.REQUEST_SHORT_AUDIO_DESCRIPTOR,
+                AudioManagerHelper.getRequestSadFormatsParams(getDevice(), false));
         String message = hdmiCecClient.checkExpectedOutput(LogicalAddress.TV, CecOperand.FEATURE_ABORT);
         assertThat(CecOperand.getOperand(CecMessage.getParams(message, 2)))
                 .isEqualTo(CecOperand.REQUEST_SHORT_AUDIO_DESCRIPTOR);
@@ -472,7 +375,7 @@
      */
     @Test
     public void cect_11_2_15_16_UnmuteForSystemAudioRequestOn() throws Exception {
-        muteDevice();
+        AudioManagerHelper.muteDevice(getDevice(), hdmiCecClient);
         sendSystemAudioModeTermination();
         String message = hdmiCecClient.checkExpectedOutput(CecOperand.SET_SYSTEM_AUDIO_MODE);
         assertThat(CecMessage.getParams(message)).isEqualTo(OFF);
@@ -482,7 +385,9 @@
                     HdmiCecConstants.PHYSICAL_ADDRESS_LENGTH));
         message = hdmiCecClient.checkExpectedOutput(CecOperand.SET_SYSTEM_AUDIO_MODE);
         assertThat(CecMessage.getParams(message)).isEqualTo(ON);
-        assertWithMessage("Device muted").that(isDeviceMuted()).isFalse();
+        assertWithMessage("Device muted")
+                .that(AudioManagerHelper.isDeviceMuted(getDevice()))
+                .isFalse();
     }
 
     /**
@@ -501,7 +406,9 @@
         sendSystemAudioModeTermination();
         message = hdmiCecClient.checkExpectedOutput(CecOperand.SET_SYSTEM_AUDIO_MODE);
         assertThat(CecMessage.getParams(message)).isEqualTo(OFF);
-        assertWithMessage("Device not muted").that(isDeviceMuted()).isTrue();
+        assertWithMessage("Device not muted")
+                .that(AudioManagerHelper.isDeviceMuted(getDevice()))
+                .isTrue();
     }
 
     /**
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemAudioControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemAudioControlTest.java
new file mode 100644
index 0000000..319afb0
--- /dev/null
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/common/HdmiCecSystemAudioControlTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hdmicec.cts.common;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import android.hdmicec.cts.AudioManagerHelper;
+import android.hdmicec.cts.BaseHdmiCecCtsTest;
+import android.hdmicec.cts.CecMessage;
+import android.hdmicec.cts.CecOperand;
+import android.hdmicec.cts.HdmiCecConstants;
+import android.hdmicec.cts.LogicalAddress;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+/** HDMI CEC test to verify system audio control commands (Section 11.1.15, 11.2.15) */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class HdmiCecSystemAudioControlTest extends BaseHdmiCecCtsTest {
+
+    public boolean isDutTv;
+
+    public HdmiCecSystemAudioControlTest() {
+        super("-t", "a");
+    }
+
+    @Rule
+    public RuleChain ruleChain =
+            RuleChain.outerRule(CecRules.requiresCec(this))
+                    .around(CecRules.requiresLeanback(this))
+                    .around(
+                            CecRules.skipDeviceType(
+                                    this, HdmiCecConstants.CEC_DEVICE_TYPE_AUDIO_SYSTEM))
+                    .around(hdmiCecClient);
+
+    @Before
+    public void initialTestSetup() throws Exception {
+        /*
+         * Set the device volume level to 20, because if the volume were 100% or 0%, it would not
+         * be possible to detect if there was an incorrect increase or decrease in level.
+         */
+        AudioManagerHelper.setDeviceVolume(getDevice(), 20);
+        TimeUnit.SECONDS.sleep(5);
+        isDutTv = (mDutLogicalAddress.getDeviceType() == HdmiCecConstants.CEC_DEVICE_TYPE_TV);
+        if (isDutTv) {
+            int initialDutVolume = AudioManagerHelper.getDutAudioVolume(getDevice());
+        }
+    }
+
+    /**
+     * Test 11.1.15-2, 11.2.15-11
+     *
+     * <p>Tests that when System Audio Control is On, the device sends {@code
+     * <USER_CONTROL_PRESSED>} and {@code <USER_CONTROL_RELEASED>} messages when the volume up and
+     * down keys are pressed on the DUT. Test also verifies that the {@code <USER_CONTROL_PRESSED>}
+     * message has the right control param.
+     */
+    @Test
+    public void cect_VolumeUpDownUserControlPressedWhenSystemAudioControlOn() throws Exception {
+        /*
+         * TODO: Remove the assumeTrue below and let test run for playback devices when b/172539380
+         * is fixed.
+         */
+        assumeTrue(
+                "Skip for playback devices (b/172539380)",
+                mDutLogicalAddress.getDeviceType()
+                        != HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE);
+        ITestDevice device = getDevice();
+        hdmiCecClient.sendCecMessage(
+                hdmiCecClient.getSelfDevice(),
+                LogicalAddress.BROADCAST,
+                CecOperand.SET_SYSTEM_AUDIO_MODE,
+                CecMessage.formatParams(1));
+        device.executeShellCommand("input keyevent KEYCODE_VOLUME_UP");
+        String message =
+                hdmiCecClient.checkExpectedOutput(
+                        hdmiCecClient.getSelfDevice(), CecOperand.USER_CONTROL_PRESSED);
+        assertThat(CecMessage.getParams(message)).isEqualTo(HdmiCecConstants.CEC_CONTROL_VOLUME_UP);
+        hdmiCecClient.checkExpectedOutput(
+                hdmiCecClient.getSelfDevice(), CecOperand.USER_CONTROL_RELEASED);
+        /* TODO: b/174733146  For TV devices, assert that the volume level has not changed. */
+
+        device.executeShellCommand("input keyevent KEYCODE_VOLUME_DOWN");
+        message =
+                hdmiCecClient.checkExpectedOutput(
+                        hdmiCecClient.getSelfDevice(), CecOperand.USER_CONTROL_PRESSED);
+        assertThat(CecMessage.getParams(message))
+                .isEqualTo(HdmiCecConstants.CEC_CONTROL_VOLUME_DOWN);
+        hdmiCecClient.checkExpectedOutput(
+                hdmiCecClient.getSelfDevice(), CecOperand.USER_CONTROL_RELEASED);
+        /* TODO: b/174733146  For TV devices, assert that the volume level has not changed. */
+    }
+
+    /**
+     * Test 11.1.15-3, 11.2.15-12
+     *
+     * <p>Tests that the device sends {@code <USER_CONTROL_PRESSED>} and {@code
+     * <USER_CONTROL_RELEASED>} messages when the mute key is pressed on the DUT. Test also verifies
+     * that the{@code <USER_CONTROL_PRESSED>} message has the right control param.
+     */
+    @Test
+    public void cect_MuteUserControlPressedWhenSystemAudioControlOn() throws Exception {
+        /*
+         * TODO: Remove the assumeTrue below and let test run for playback devices when b/172539380
+         * is fixed.
+         */
+        assumeTrue(
+                "Skip for playback devices (b/172539380)",
+                mDutLogicalAddress.getDeviceType()
+                        != HdmiCecConstants.CEC_DEVICE_TYPE_PLAYBACK_DEVICE);
+        ITestDevice device = getDevice();
+        hdmiCecClient.sendCecMessage(
+                hdmiCecClient.getSelfDevice(),
+                LogicalAddress.BROADCAST,
+                CecOperand.SET_SYSTEM_AUDIO_MODE,
+                CecMessage.formatParams(1));
+        device.executeShellCommand("input keyevent KEYCODE_VOLUME_MUTE");
+        String message =
+                hdmiCecClient.checkExpectedOutput(
+                        hdmiCecClient.getSelfDevice(), CecOperand.USER_CONTROL_PRESSED);
+        assertThat(CecMessage.getParams(message)).isEqualTo(HdmiCecConstants.CEC_CONTROL_MUTE);
+        hdmiCecClient.checkExpectedOutput(
+                hdmiCecClient.getSelfDevice(), CecOperand.USER_CONTROL_RELEASED);
+        /* TODO: b/174733146  For TV devices, assert that the volume level has not changed. */
+    }
+
+    /**
+     * Test 11.1.15-4, 11.2.15-10
+     *
+     * <p>Tests that the device sends a {@code <GIVE_SYSTEM_AUDIO_STATUS>} message when brought out
+     * of standby
+     */
+    @Test
+    public void cect_GiveSystemAudioModeStatus() throws Exception {
+        ITestDevice device = getDevice();
+        /* Home Key to prevent device from going to deep suspend state */
+        device.executeShellCommand("input keyevent KEYCODE_HOME");
+        device.executeShellCommand("input keyevent KEYCODE_SLEEP");
+        device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        hdmiCecClient.checkExpectedOutput(
+                hdmiCecClient.getSelfDevice(), CecOperand.GIVE_SYSTEM_AUDIO_MODE_STATUS);
+    }
+}
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemAudioControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemAudioControlTest.java
deleted file mode 100644
index 95baefc..0000000
--- a/hostsidetests/hdmicec/src/android/hdmicec/cts/playback/HdmiCecSystemAudioControlTest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.hdmicec.cts.playback;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.hdmicec.cts.BaseHdmiCecCtsTest;
-import android.hdmicec.cts.CecMessage;
-import android.hdmicec.cts.CecOperand;
-import android.hdmicec.cts.HdmiCecClientWrapper;
-import android.hdmicec.cts.HdmiCecConstants;
-import android.hdmicec.cts.LogicalAddress;
-import android.hdmicec.cts.RequiredPropertyRule;
-import android.hdmicec.cts.RequiredFeatureRule;
-
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-import org.junit.Test;
-
-/** HDMI CEC test to verify system audio control commands (Section 11.2.15) */
-@RunWith(DeviceJUnit4ClassRunner.class)
-public final class HdmiCecSystemAudioControlTest extends BaseHdmiCecCtsTest {
-
-    private static final LogicalAddress PLAYBACK_DEVICE = LogicalAddress.PLAYBACK_1;
-
-    public HdmiCecSystemAudioControlTest() {
-        super(LogicalAddress.PLAYBACK_1, "-t", "a");
-    }
-
-    @Rule
-    public RuleChain ruleChain =
-        RuleChain
-            .outerRule(CecRules.requiresCec(this))
-            .around(CecRules.requiresLeanback(this))
-            .around(CecRules.requiresDeviceType(this, LogicalAddress.PLAYBACK_1))
-            .around(hdmiCecClient);
-
-    /**
-     * Test 11.2.15-10
-     * Tests that the device sends a <GIVE_SYSTEM_AUDIO_STATUS> message when brought out of standby
-     */
-    @Test
-    public void cect_11_2_15_10_GiveSystemAudioModeStatus() throws Exception {
-        ITestDevice device = getDevice();
-        /* Home Key to prevent device from going to deep suspend state */
-        device.executeShellCommand("input keyevent KEYCODE_HOME");
-        device.executeShellCommand("input keyevent KEYCODE_SLEEP");
-        device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
-        hdmiCecClient.checkExpectedOutput(LogicalAddress.AUDIO_SYSTEM,
-                CecOperand.GIVE_SYSTEM_AUDIO_MODE_STATUS);
-    }
-
-    /**
-     * Test 11.2.15-11
-     * Tests that the device sends <USER_CONTROL_PRESSED> and <USER_CONTROL_RELEASED> messages when
-     * the volume up and down keys are pressed on the DUT. Test also verifies that the
-     * <USER_CONTROL_PRESSED> message has the right control param.
-     */
-    @Ignore("b/162836413")
-    @Test
-    public void cect_11_2_15_11_VolumeUpDownUserControlPressed() throws Exception {
-        ITestDevice device = getDevice();
-        hdmiCecClient.sendCecMessage(LogicalAddress.AUDIO_SYSTEM, LogicalAddress.BROADCAST,
-                CecOperand.SET_SYSTEM_AUDIO_MODE, CecMessage.formatParams(1));
-        device.executeShellCommand("input keyevent KEYCODE_VOLUME_UP");
-        String message = hdmiCecClient.checkExpectedOutput(LogicalAddress.AUDIO_SYSTEM,
-                CecOperand.USER_CONTROL_PRESSED);
-        assertThat(CecMessage.getParams(message))
-                .isEqualTo(HdmiCecConstants.CEC_CONTROL_VOLUME_UP);
-        hdmiCecClient.checkExpectedOutput(LogicalAddress.AUDIO_SYSTEM, CecOperand.USER_CONTROL_RELEASED);
-
-
-        device.executeShellCommand("input keyevent KEYCODE_VOLUME_DOWN");
-        message = hdmiCecClient.checkExpectedOutput(LogicalAddress.AUDIO_SYSTEM,
-                CecOperand.USER_CONTROL_PRESSED);
-        assertThat(CecMessage.getParams(message))
-                .isEqualTo(HdmiCecConstants.CEC_CONTROL_VOLUME_DOWN);
-        hdmiCecClient.checkExpectedOutput(LogicalAddress.AUDIO_SYSTEM, CecOperand.USER_CONTROL_RELEASED);
-    }
-
-    /**
-     * Test 11.2.15-12
-     * Tests that the device sends <USER_CONTROL_PRESSED> and <USER_CONTROL_RELEASED> messages when
-     * the mute key is pressed on the DUT. Test also verifies that the <USER_CONTROL_PRESSED>
-     * message has the right control param.
-     */
-    @Ignore("b/162836413")
-    @Test
-    public void cect_11_2_15_12_MuteUserControlPressed() throws Exception {
-        ITestDevice device = getDevice();
-        hdmiCecClient.sendCecMessage(LogicalAddress.AUDIO_SYSTEM, LogicalAddress.BROADCAST,
-                CecOperand.SET_SYSTEM_AUDIO_MODE, CecMessage.formatParams(1));
-        device.executeShellCommand("input keyevent KEYCODE_VOLUME_MUTE");
-        String message = hdmiCecClient.checkExpectedOutput(LogicalAddress.AUDIO_SYSTEM,
-                CecOperand.USER_CONTROL_PRESSED);
-        assertThat(CecMessage.getParams(message)).isEqualTo(HdmiCecConstants.CEC_CONTROL_MUTE);
-        hdmiCecClient.checkExpectedOutput(LogicalAddress.AUDIO_SYSTEM, CecOperand.USER_CONTROL_RELEASED);
-    }
-}
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemAudioControlTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemAudioControlTest.java
new file mode 100644
index 0000000..4a4bf92
--- /dev/null
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecSystemAudioControlTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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 android.hdmicec.cts.tv;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.hdmicec.cts.AudioManagerHelper;
+import android.hdmicec.cts.BaseHdmiCecCtsTest;
+import android.hdmicec.cts.CecMessage;
+import android.hdmicec.cts.CecOperand;
+import android.hdmicec.cts.LogicalAddress;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+/** HDMI CEC test to verify system audio control commands tests (Section 11.1.15) */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class HdmiCecSystemAudioControlTest extends BaseHdmiCecCtsTest {
+
+    private static final int ON = 0x1;
+    private static final int OFF = 0x0;
+
+    @Rule
+    public RuleChain ruleChain =
+            RuleChain.outerRule(CecRules.requiresCec(this))
+                    .around(CecRules.requiresLeanback(this))
+                    .around(CecRules.requiresDeviceType(this, LogicalAddress.TV))
+                    .around(hdmiCecClient);
+
+    public HdmiCecSystemAudioControlTest() {
+        super(LogicalAddress.TV, "-t", "a");
+    }
+
+    /**
+     * Test 11.1.15-7
+     *
+     * <p>Tests that the DUT mutes its volume when the DUT receives a broadcast {@code <Set System
+     * Audio Mode>} ["On"] message
+     */
+    @Ignore("b/174733146")
+    @Test
+    public void cect_11_1_15_7_DutMutesForSetSystemAudioModeOn() throws Exception {
+        /*
+         * TODO: Call HdmiCecLocalDeviceTv.setSystemAudioMode(false) instead to turn off system
+         * audio mode after permission issue is sorted.
+         */
+        hdmiCecClient.sendCecMessage(
+                hdmiCecClient.getSelfDevice(),
+                LogicalAddress.BROADCAST,
+                CecOperand.SET_SYSTEM_AUDIO_MODE,
+                CecMessage.formatParams(OFF));
+        hdmiCecClient.sendCecMessage(
+                hdmiCecClient.getSelfDevice(),
+                LogicalAddress.BROADCAST,
+                CecOperand.SET_SYSTEM_AUDIO_MODE,
+                CecMessage.formatParams(ON));
+        assertWithMessage("Device is not muted")
+                .that(AudioManagerHelper.isDeviceMuted(getDevice()))
+                .isTrue();
+    }
+
+    /**
+     * Test 11.1.15-8
+     *
+     * <p>Tests that the DUT unmutes its volume when the DUT receives a broadcast {@code <Set System
+     * Audio Mode>} ["Off"] message
+     */
+    @Ignore("b/174733146")
+    @Test
+    public void cect_11_1_15_8_DutUnmutesForSetSystemAudioModeOff() throws Exception {
+        /*
+         * TODO: Call HdmiCecLocalDeviceTv.setSystemAudioMode(true) instead to turn off system
+         * audio mode after permission issue is sorted.
+         */
+        hdmiCecClient.sendCecMessage(
+                hdmiCecClient.getSelfDevice(),
+                LogicalAddress.BROADCAST,
+                CecOperand.SET_SYSTEM_AUDIO_MODE,
+                CecMessage.formatParams(ON));
+        hdmiCecClient.sendCecMessage(
+                hdmiCecClient.getSelfDevice(),
+                LogicalAddress.BROADCAST,
+                CecOperand.SET_SYSTEM_AUDIO_MODE,
+                CecMessage.formatParams(OFF));
+        assertWithMessage("Device is muted")
+                .that(AudioManagerHelper.isDeviceMuted(getDevice()))
+                .isFalse();
+    }
+}
diff --git a/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvOneTouchPlayTest.java b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvOneTouchPlayTest.java
new file mode 100644
index 0000000..ee6fafa
--- /dev/null
+++ b/hostsidetests/hdmicec/src/android/hdmicec/cts/tv/HdmiCecTvOneTouchPlayTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hdmicec.cts.tv;
+
+import android.hdmicec.cts.BaseHdmiCecCtsTest;
+import android.hdmicec.cts.CecOperand;
+import android.hdmicec.cts.LogicalAddress;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** HDMI CEC test to test One Touch Play features (Section 11.1.1) */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class HdmiCecTvOneTouchPlayTest extends BaseHdmiCecCtsTest {
+
+    private static final LogicalAddress TV_DEVICE = LogicalAddress.TV;
+    private static final int WAIT_TIME_MS = 300;
+    List<LogicalAddress> testDevices = new ArrayList<>();
+
+    public HdmiCecTvOneTouchPlayTest() {
+        /* Start the client as recorder, tuner and playback devices */
+        super(TV_DEVICE, "-t", "r", "-t", "t", "-t", "p");
+        testDevices.add(LogicalAddress.RECORDER_1);
+        testDevices.add(LogicalAddress.TUNER_1);
+        testDevices.add(LogicalAddress.PLAYBACK_1);
+    }
+
+    @Rule
+    public RuleChain ruleChain =
+            RuleChain.outerRule(CecRules.requiresCec(this))
+                    .around(CecRules.requiresLeanback(this))
+                    .around(CecRules.requiresDeviceType(this, TV_DEVICE))
+                    .around(hdmiCecClient);
+
+    /**
+     * Test 11.1.1-1
+     *
+     * <p>Tests that the DUT responds to {@code <Image View On>} message correctly when the message
+     * is sent from logical addresses 0x1, 0x3 and 0x4
+     */
+    @Test
+    public void cect_11_1_1_1_RespondToImageViewOn() throws Exception {
+        for (LogicalAddress testDevice : testDevices) {
+            hdmiCecClient.sendCecMessage(testDevice, LogicalAddress.TV, CecOperand.IMAGE_VIEW_ON);
+            TimeUnit.MILLISECONDS.sleep(WAIT_TIME_MS);
+            hdmiCecClient.broadcastActiveSource(testDevice, hdmiCecClient.getPhysicalAddress());
+            hdmiCecClient.checkOutputDoesNotContainMessage(testDevice, CecOperand.FEATURE_ABORT);
+            assertWithMessage(
+                            "Device has not registered expected logical address as active source.")
+                    .that(getDumpsysActiveSourceLogicalAddress())
+                    .isEqualTo(testDevice);
+        }
+    }
+
+    /**
+     * Test 11.1.1-2
+     *
+     * <p>Tests that the DUT responds to {@code <Text View On>} message correctly when the message
+     * is sent from logical addresses 0x1, 0x3 and 0x4
+     */
+    @Test
+    public void cect_11_1_1_2_RespondToTextViewOn() throws Exception {
+        for (LogicalAddress testDevice : testDevices) {
+            hdmiCecClient.sendCecMessage(testDevice, LogicalAddress.TV, CecOperand.TEXT_VIEW_ON);
+            TimeUnit.MILLISECONDS.sleep(WAIT_TIME_MS);
+            hdmiCecClient.broadcastActiveSource(testDevice, hdmiCecClient.getPhysicalAddress());
+            hdmiCecClient.checkOutputDoesNotContainMessage(testDevice, CecOperand.FEATURE_ABORT);
+            assertWithMessage(
+                            "Device has not registered expected logical address as active source.")
+                    .that(getDumpsysActiveSourceLogicalAddress())
+                    .isEqualTo(testDevice);
+        }
+    }
+}
diff --git a/tests/tests/identity/src/android/security/identity/cts/DynamicAuthTest.java b/tests/tests/identity/src/android/security/identity/cts/DynamicAuthTest.java
index c5c5cd1..f61273e 100644
--- a/tests/tests/identity/src/android/security/identity/cts/DynamicAuthTest.java
+++ b/tests/tests/identity/src/android/security/identity/cts/DynamicAuthTest.java
@@ -25,6 +25,7 @@
 
 import android.content.Context;
 
+import android.os.SystemClock;
 import android.security.identity.EphemeralPublicKeyNotFoundException;
 import android.security.identity.IdentityCredential;
 import android.security.identity.IdentityCredentialException;
@@ -43,6 +44,7 @@
 import java.security.SignatureException;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -455,6 +457,140 @@
         store.deleteCredentialByName(credentialName);
     }
 
+
+    @Test
+    public void dynamicAuthWithExpirationTest() throws Exception {
+        assumeTrue("IC HAL is not implemented", Util.isHalImplemented());
+
+        Context appContext = InstrumentationRegistry.getTargetContext();
+        IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext);
+        assumeTrue(
+            "IdentityCredential.storeStaticAuthenticationData(X509Certificate, Instant, byte[]) " +
+            "not supported",
+            Util.getFeatureVersion() >= 202101);
+
+        String credentialName = "test";
+
+        store.deleteCredentialByName(credentialName);
+        Collection<X509Certificate> certChain = ProvisioningTest.createCredential(store,
+                credentialName);
+
+        IdentityCredential credential = store.getCredentialByName(credentialName,
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+        assertNotNull(credential);
+
+        credential.setAvailableAuthenticationKeys(3, 5);
+
+        Collection<X509Certificate> certificates = null;
+        certificates = credential.getAuthKeysNeedingCertification();
+        assertEquals(3, certificates.size());
+
+        // Endorse an auth-key but set expiration to 10 seconds in the future.
+        //
+        Instant now = Instant.now();
+        Instant tenSecondsFromNow = now.plusSeconds(10);
+        try {
+            X509Certificate key0Cert = certificates.iterator().next();
+            credential.storeStaticAuthenticationData(key0Cert,
+                    tenSecondsFromNow,
+                    new byte[]{52, 53, 44});
+            certificates = credential.getAuthKeysNeedingCertification();
+        } catch (IdentityCredentialException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+        assertEquals(2, certificates.size());
+        assertArrayEquals(
+                new int[]{0, 0, 0},
+                credential.getAuthenticationDataUsageCount());
+        // Check that presentation works.
+        try {
+            IdentityCredential tc = store.getCredentialByName(credentialName,
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+            KeyPair ekp = tc.createEphemeralKeyPair();
+            KeyPair rekp = Util.createEphemeralKeyPair();
+            tc.setReaderEphemeralPublicKey(rekp.getPublic());
+            byte[] st = Util.buildSessionTranscript(ekp);
+            Map<String, Collection<String>> etr = new LinkedHashMap<>();
+            etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name"));
+            ResultData rd = tc.getEntries(
+                Util.createItemsRequest(etr, null),
+                etr,
+                st,
+                null);
+        } catch (IdentityCredentialException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+        credential = store.getCredentialByName(credentialName,
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+        assertArrayEquals(
+                new int[]{1, 0, 0},
+                credential.getAuthenticationDataUsageCount());
+
+        SystemClock.sleep(11 * 1000);
+
+        certificates = credential.getAuthKeysNeedingCertification();
+        assertEquals(3, certificates.size());
+
+        // Check that presentation now fails..
+        try {
+            IdentityCredential tc = store.getCredentialByName(credentialName,
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+            KeyPair ekp = tc.createEphemeralKeyPair();
+            KeyPair rekp = Util.createEphemeralKeyPair();
+            tc.setReaderEphemeralPublicKey(rekp.getPublic());
+            byte[] st = Util.buildSessionTranscript(ekp);
+            Map<String, Collection<String>> etr = new LinkedHashMap<>();
+            etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name"));
+            ResultData rd = tc.getEntries(
+                Util.createItemsRequest(etr, null),
+                etr,
+                st,
+                null);
+            assertTrue(false);
+        } catch (NoAuthenticationKeyAvailableException e) {
+            // This is the expected path...
+        } catch (IdentityCredentialException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+        credential = store.getCredentialByName(credentialName,
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+        assertArrayEquals(
+                new int[]{1, 0, 0},
+                credential.getAuthenticationDataUsageCount());
+
+        // Check that it works if we use setAllowUsingExpiredKeys(true)
+        try {
+            IdentityCredential tc = store.getCredentialByName(credentialName,
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+            tc.setAllowUsingExpiredKeys(true);   // <-- this is the call that makes the difference!
+            KeyPair ekp = tc.createEphemeralKeyPair();
+            KeyPair rekp = Util.createEphemeralKeyPair();
+            tc.setReaderEphemeralPublicKey(rekp.getPublic());
+            byte[] st = Util.buildSessionTranscript(ekp);
+            Map<String, Collection<String>> etr = new LinkedHashMap<>();
+            etr.put("org.iso.18013-5.2019", Arrays.asList("First name", "Last name"));
+            ResultData rd = tc.getEntries(
+                Util.createItemsRequest(etr, null),
+                etr,
+                st,
+                null);
+        } catch (IdentityCredentialException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+        credential = store.getCredentialByName(credentialName,
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+        assertArrayEquals(
+                new int[]{2, 0, 0},
+                credential.getAuthenticationDataUsageCount());
+
+        // ... and we're done. Clean up after ourselves.
+        store.deleteCredentialByName(credentialName);
+    }
+
     // TODO: test storeStaticAuthenticationData() throwing UnknownAuthenticationKeyException
     // on an unknown auth key
 }
diff --git a/tests/tests/identity/src/android/security/identity/cts/ProvisioningTest.java b/tests/tests/identity/src/android/security/identity/cts/ProvisioningTest.java
index 519e915..b78cb98 100644
--- a/tests/tests/identity/src/android/security/identity/cts/ProvisioningTest.java
+++ b/tests/tests/identity/src/android/security/identity/cts/ProvisioningTest.java
@@ -48,6 +48,7 @@
 import java.io.ByteArrayOutputStream;
 import java.security.KeyPair;
 import java.security.InvalidKeyException;
+import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.X509Certificate;
@@ -409,7 +410,7 @@
     }
 
     @Test
-    public void deleteCredential()
+    public void deleteCredentialByName()
             throws IdentityCredentialException, CborException, CertificateEncodingException {
         assumeTrue("IC HAL is not implemented", Util.isHalImplemented());
 
@@ -447,6 +448,92 @@
     }
 
     @Test
+    public void deleteCredential()
+            throws IdentityCredentialException, CborException, CertificateEncodingException {
+        assumeTrue("IC HAL is not implemented", Util.isHalImplemented());
+
+        Context appContext = InstrumentationRegistry.getTargetContext();
+        IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext);
+        assumeTrue("IdentityCredential.delete() not supported", Util.getFeatureVersion() >= 202101);
+
+        store.deleteCredentialByName("test");
+        assertNull(store.deleteCredentialByName("test"));
+        Collection<X509Certificate> certificateChain = createCredential(store, "test");
+
+        // Deleting the credential involves destroying the keys referenced in the returned
+        // certificateChain... so get an encoded blob we can turn into a X509 cert when
+        // checking the deletion receipt below, post-deletion.
+        byte[] encodedCredentialCert = certificateChain.iterator().next().getEncoded();
+
+        IdentityCredential credential = store.getCredentialByName("test",
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+        assertNotNull(credential);
+
+        byte[] challenge = new byte[]{0x20, 0x21};
+        byte[] proofOfDeletionSignature = credential.delete(challenge);
+        byte[] proofOfDeletion = Util.coseSign1GetData(proofOfDeletionSignature);
+
+        // Check the returned CBOR is what is expected.
+        String pretty = Util.cborPrettyPrint(proofOfDeletion);
+        assertEquals("['ProofOfDeletion', 'org.iso.18013-5.2019.mdl', [0x20, 0x21], false]", pretty);
+
+        try {
+            assertTrue(Util.coseSign1CheckSignature(
+                proofOfDeletionSignature,
+                new byte[0], // Additional data
+                certificateChain.iterator().next().getPublicKey()));
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+
+        // Finally, check deleting an already deleted credential returns the expected.
+        assertNull(store.deleteCredentialByName("test"));
+    }
+
+    @Test
+    public void proofOfOwnership()
+            throws IdentityCredentialException, CborException, CertificateEncodingException {
+        assumeTrue("IC HAL is not implemented", Util.isHalImplemented());
+
+        Context appContext = InstrumentationRegistry.getTargetContext();
+        IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext);
+        assumeTrue("IdentityCredential.proveOwnership() not supported", Util.getFeatureVersion() >= 202101);
+
+        store.deleteCredentialByName("test");
+        assertNull(store.deleteCredentialByName("test"));
+        Collection<X509Certificate> certificateChain = createCredential(store, "test");
+
+        byte[] encodedCredentialCert = certificateChain.iterator().next().getEncoded();
+
+        IdentityCredential credential = store.getCredentialByName("test",
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+        assertNotNull(credential);
+
+        byte[] challenge = new byte[]{0x12, 0x22};
+        byte[] proofOfOwnershipSignature = credential.proveOwnership(challenge);
+        byte[] proofOfOwnership = Util.coseSign1GetData(proofOfOwnershipSignature);
+
+        // Check the returned CBOR is what is expected.
+        String pretty = Util.cborPrettyPrint(proofOfOwnership);
+        assertEquals("['ProofOfOwnership', 'org.iso.18013-5.2019.mdl', [0x12, 0x22], false]",
+                pretty);
+
+        try {
+            assertTrue(Util.coseSign1CheckSignature(
+                proofOfOwnershipSignature,
+                new byte[0], // Additional data
+                certificateChain.iterator().next().getPublicKey()));
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+
+        // Finally, check the credential is still there
+        assertNotNull(store.deleteCredentialByName("test"));
+    }
+
+    @Test
     public void testProvisionAndRetrieve() throws IdentityCredentialException, CborException {
         assumeTrue("IC HAL is not implemented", Util.isHalImplemented());
 
@@ -1016,4 +1103,201 @@
         store.deleteCredentialByName("test");
     }
 
+    @Test
+    public void testUpdateCredential() throws IdentityCredentialException, CborException, NoSuchAlgorithmException {
+        assumeTrue("IC HAL is not implemented", Util.isHalImplemented());
+
+        Context appContext = InstrumentationRegistry.getTargetContext();
+        IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext);
+        assumeTrue("IdentityCredential.update() not supported", Util.getFeatureVersion() >= 202101);
+
+        // Create the credential...
+        //
+        String credentialName = "test";
+        String exampleDocType = "org.example.myDocType";
+        String exampleNs = "org.example.ns";
+        byte[] challenge = {0x01, 0x02};
+        int acpId = 3;
+        store.deleteCredentialByName(credentialName);
+        WritableIdentityCredential wc = store.createCredential(credentialName, exampleDocType);
+        Collection<X509Certificate> certChain = wc.getCredentialKeyCertificateChain(challenge);
+        AccessControlProfile noAuthProfile =
+                new AccessControlProfile.Builder(new AccessControlProfileId(acpId))
+                        .setUserAuthenticationRequired(false)
+                        .build();
+        Collection<AccessControlProfileId> idsNoAuth = new ArrayList<AccessControlProfileId>();
+        idsNoAuth.add(new AccessControlProfileId(acpId));
+        PersonalizationData personalizationData =
+                new PersonalizationData.Builder()
+                        .addAccessControlProfile(noAuthProfile)
+                        .putEntry(exampleNs, "first_name", idsNoAuth, Util.cborEncodeString("John"))
+                        .putEntry(exampleNs, "last_name", idsNoAuth, Util.cborEncodeString("Smith"))
+                        .build();
+        byte[] proofOfProvisioningSignature = wc.personalize(personalizationData);
+        byte[] proofOfProvisioning = Util.coseSign1GetData(proofOfProvisioningSignature);
+        byte[] proofOfProvisioningSha256 = MessageDigest.getInstance("SHA-256").digest(proofOfProvisioning);
+        String pretty = "";
+        try {
+            pretty = Util.cborPrettyPrint(proofOfProvisioning);
+        } catch (CborException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+        assertEquals("[\n"
+                + "  'ProofOfProvisioning',\n"
+                + "  '" + exampleDocType + "',\n"
+                + "  [\n"
+                + "    {\n"
+                + "      'id' : " + acpId + "\n"
+                + "    }\n"
+                + "  ],\n"
+                + "  {\n"
+                + "    '" + exampleNs + "' : [\n"
+                + "      {\n"
+                + "        'name' : 'first_name',\n"
+                + "        'value' : 'John',\n"
+                + "        'accessControlProfiles' : [" + acpId + "]\n"
+                + "      },\n"
+                + "      {\n"
+                + "        'name' : 'last_name',\n"
+                + "        'value' : 'Smith',\n"
+                + "        'accessControlProfiles' : [" + acpId + "]\n"
+                + "      }\n"
+                + "    ]\n"
+                + "  },\n"
+                + "  false\n"
+                + "]", pretty);
+        try {
+            assertTrue(Util.coseSign1CheckSignature(
+                proofOfProvisioningSignature,
+                new byte[0], // Additional data
+                certChain.iterator().next().getPublicKey()));
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+
+        IdentityCredential credential = store.getCredentialByName("test",
+                IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256);
+
+        // Configure to use 3 auth keys and endorse all of them
+        credential.setAvailableAuthenticationKeys(3, 5);
+        Collection<X509Certificate> certificates = credential.getAuthKeysNeedingCertification();
+        assertEquals(3, certificates.size());
+        for (X509Certificate cert : certificates) {
+            credential.storeStaticAuthenticationData(cert, new byte[]{1, 2});
+            // Check each cert has the correct ProofOfProvisioning SHA-256 in the
+            // ProofOfBinding CBOR stored at OID 1.3.6.1.4.1.11129.2.1.26
+            byte[] popSha256FromCert = Util.getPopSha256FromAuthKeyCert(cert);
+            assertArrayEquals(popSha256FromCert, proofOfProvisioningSha256);
+        }
+        assertEquals(0, credential.getAuthKeysNeedingCertification().size());
+
+        // Update the credential
+        AccessControlProfile updNoAuthProfile =
+                new AccessControlProfile.Builder(new AccessControlProfileId(31))
+                        .setUserAuthenticationRequired(false)
+                        .build();
+        Collection<AccessControlProfileId> updIds = new ArrayList<AccessControlProfileId>();
+        updIds.add(new AccessControlProfileId(31));
+        String updNs = "org.iso.other_ns";
+        PersonalizationData updPd =
+                new PersonalizationData.Builder()
+                        .addAccessControlProfile(updNoAuthProfile)
+                        .putEntry(updNs, "first_name", updIds, Util.cborEncodeString("Lawrence"))
+                        .putEntry(updNs, "last_name", updIds, Util.cborEncodeString("Waterhouse"))
+                        .build();
+        byte[] updProofOfProvisioningSignature = credential.update(updPd);
+
+        // Check the ProofOfProvisioning for the updated data (contents _and_ signature)
+        byte[] updProofOfProvisioning = Util.coseSign1GetData(updProofOfProvisioningSignature);
+        byte[] updProofOfProvisioningSha256 = MessageDigest.getInstance("SHA-256").digest(updProofOfProvisioning);
+        try {
+            pretty = Util.cborPrettyPrint(updProofOfProvisioning);
+        } catch (CborException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+        assertEquals("[\n"
+                + "  'ProofOfProvisioning',\n"
+                + "  '" + exampleDocType + "',\n"
+                + "  [\n"
+                + "    {\n"
+                + "      'id' : 31\n"
+                + "    }\n"
+                + "  ],\n"
+                + "  {\n"
+                + "    'org.iso.other_ns' : [\n"
+                + "      {\n"
+                + "        'name' : 'first_name',\n"
+                + "        'value' : 'Lawrence',\n"
+                + "        'accessControlProfiles' : [31]\n"
+                + "      },\n"
+                + "      {\n"
+                + "        'name' : 'last_name',\n"
+                + "        'value' : 'Waterhouse',\n"
+                + "        'accessControlProfiles' : [31]\n"
+                + "      }\n"
+                + "    ]\n"
+                + "  },\n"
+                + "  false\n"
+                + "]", pretty);
+        try {
+            assertTrue(Util.coseSign1CheckSignature(
+                updProofOfProvisioningSignature,
+                new byte[0], // Additional data
+                certChain.iterator().next().getPublicKey()));
+        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+            e.printStackTrace();
+            assertTrue(false);
+        }
+        // Check the returned CredentialKey cert chain from the now updated
+        // IdentityCredential matches the original certificate chain.
+        //
+        Collection<X509Certificate> readBackCertChain =
+                credential.getCredentialKeyCertificateChain();
+        assertEquals(certChain.size(), readBackCertChain.size());
+        Iterator<X509Certificate> it = readBackCertChain.iterator();
+        for (X509Certificate expectedCert : certChain) {
+            X509Certificate readBackCert = it.next();
+            assertEquals(expectedCert, readBackCert);
+        }
+
+        // Check that the credential is still configured to use 3 auth keys and
+        // that they all need replacement... then check and endorse the
+        // replacements
+        Collection<X509Certificate> updCertificates = credential.getAuthKeysNeedingCertification();
+        assertEquals(3, updCertificates.size());
+        for (X509Certificate cert : updCertificates) {
+            credential.storeStaticAuthenticationData(cert, new byte[]{1, 2});
+            // Check each cert has the correct - *updated* - ProofOfProvisioning SHA-256 in the
+            // ProofOfBinding CBOR stored at OID 1.3.6.1.4.1.11129.2.1.26
+            byte[] popSha256FromCert = Util.getPopSha256FromAuthKeyCert(cert);
+            assertArrayEquals(popSha256FromCert, updProofOfProvisioningSha256);
+        }
+        assertEquals(0, credential.getAuthKeysNeedingCertification().size());
+
+        // Check we can read back the updated data and it matches what we
+        // updated it to.
+        Map<String, Collection<String>> entriesToRequest = new LinkedHashMap<>();
+        entriesToRequest.put(updNs,
+                Arrays.asList("first_name",
+                        "last_name"));
+        ResultData rd = credential.getEntries(
+                Util.createItemsRequest(entriesToRequest, null),
+                entriesToRequest,
+                null,
+                null);
+
+        Collection<String> resultNamespaces = rd.getNamespaces();
+        assertEquals(resultNamespaces.size(), 1);
+        assertEquals(updNs, resultNamespaces.iterator().next());
+        assertEquals(2, rd.getEntryNames(updNs).size());
+
+        assertEquals("Lawrence", Util.getStringEntry(rd, updNs, "first_name"));
+        assertEquals("Waterhouse", Util.getStringEntry(rd, updNs, "last_name"));
+
+        store.deleteCredentialByName("test");
+    }
+
 }
diff --git a/tests/tests/identity/src/android/security/identity/cts/Util.java b/tests/tests/identity/src/android/security/identity/cts/Util.java
index bbd6ee7..733a401 100644
--- a/tests/tests/identity/src/android/security/identity/cts/Util.java
+++ b/tests/tests/identity/src/android/security/identity/cts/Util.java
@@ -20,6 +20,8 @@
 import android.security.identity.IdentityCredentialStore;
 
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.FeatureInfo;
 import android.os.SystemProperties;
 import android.security.keystore.KeyProperties;
 import android.util.Log;
@@ -68,6 +70,9 @@
 import java.security.interfaces.ECPublicKey;
 import java.security.spec.ECPoint;
 
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1OctetString;
+
 import co.nstant.in.cbor.CborBuilder;
 import co.nstant.in.cbor.CborDecoder;
 import co.nstant.in.cbor.CborEncoder;
@@ -90,6 +95,40 @@
 class Util {
     private static final String TAG = "Util";
 
+    // Returns 0 if not implemented. Otherwise returns the feature version.
+    //
+    static int getFeatureVersion() {
+        Context appContext = InstrumentationRegistry.getTargetContext();
+        PackageManager pm = appContext.getPackageManager();
+
+        int featureVersionFromPm = 0;
+        if (pm.hasSystemFeature(PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE)) {
+            FeatureInfo info = null;
+            FeatureInfo[] infos = pm.getSystemAvailableFeatures();
+            for (int n = 0; n < infos.length; n++) {
+                FeatureInfo i = infos[n];
+                if (i.name.equals(PackageManager.FEATURE_IDENTITY_CREDENTIAL_HARDWARE)) {
+                    info = i;
+                    break;
+                }
+            }
+            if (info != null) {
+                featureVersionFromPm = info.version;
+            }
+        }
+
+        // Use of the system feature is not required since Android 12. So for Android 11
+        // return 202009 which is the feature version shipped with Android 11.
+        if (featureVersionFromPm == 0) {
+            IdentityCredentialStore store = IdentityCredentialStore.getInstance(appContext);
+            if (store != null) {
+                featureVersionFromPm = 202009;
+            }
+        }
+
+        return featureVersionFromPm;
+    }
+
     static byte[] canonicalizeCbor(byte[] encodedCbor) throws CborException {
         ByteArrayInputStream bais = new ByteArrayInputStream(encodedCbor);
         List<DataItem> dataItems = new CborDecoder(bais).decode();
@@ -1262,4 +1301,63 @@
         }
         return false;
     }
+
+    // Returns true if, and only if, the Direct Access Identity Credential HAL (and credstore) is
+    // implemented on the device under test.
+    static boolean isDirectAccessHalImplemented() {
+        Context appContext = InstrumentationRegistry.getTargetContext();
+        IdentityCredentialStore store = IdentityCredentialStore.getDirectAccessInstance(appContext);
+        if (store != null) {
+            return true;
+        }
+        return false;
+    }
+
+    static byte[] getPopSha256FromAuthKeyCert(X509Certificate cert) {
+        byte[] octetString = cert.getExtensionValue("1.3.6.1.4.1.11129.2.1.26");
+        if (octetString == null) {
+            return null;
+        }
+        Util.hexdump("octetString", octetString);
+
+        try {
+            ASN1InputStream asn1InputStream = new ASN1InputStream(octetString);
+            byte[] cborBytes = ((ASN1OctetString) asn1InputStream.readObject()).getOctets();
+            Util.hexdump("cborBytes", cborBytes);
+
+            ByteArrayInputStream bais = new ByteArrayInputStream(cborBytes);
+            List<DataItem> dataItems = new CborDecoder(bais).decode();
+            if (dataItems.size() != 1) {
+                throw new RuntimeException("Expected 1 item, found " + dataItems.size());
+            }
+            if (!(dataItems.get(0) instanceof co.nstant.in.cbor.model.Array)) {
+                throw new RuntimeException("Item is not a map");
+            }
+            co.nstant.in.cbor.model.Array array = (co.nstant.in.cbor.model.Array) dataItems.get(0);
+            List<DataItem> items = array.getDataItems();
+            if (items.size() < 2) {
+                throw new RuntimeException("Expected at least 2 array items, found " + items.size());
+            }
+            if (!(items.get(0) instanceof UnicodeString)) {
+                throw new RuntimeException("First array item is not a string");
+            }
+            String id = ((UnicodeString) items.get(0)).getString();
+            if (!id.equals("ProofOfBinding")) {
+                throw new RuntimeException("Expected ProofOfBinding, got " + id);
+            }
+            if (!(items.get(1) instanceof ByteString)) {
+                throw new RuntimeException("Second array item is not a bytestring");
+            }
+            byte[] popSha256 = ((ByteString) items.get(1)).getBytes();
+            if (popSha256.length != 32) {
+                throw new RuntimeException("Expected bstr to be 32 bytes, it is " + popSha256.length);
+            }
+            return popSha256;
+        } catch (IOException e) {
+            throw new RuntimeException("Error decoding extension data", e);
+        } catch (CborException e) {
+            throw new RuntimeException("Error decoding data", e);
+        }
+    }
+
 }
diff --git a/tests/tests/permission2/res/raw/android_manifest.xml b/tests/tests/permission2/res/raw/android_manifest.xml
index 3b57336..e0c8c1e 100644
--- a/tests/tests/permission2/res/raw/android_manifest.xml
+++ b/tests/tests/permission2/res/raw/android_manifest.xml
@@ -4460,7 +4460,7 @@
     <!-- Allows access to keyguard secure storage.  Only allowed for system processes.
         @hide -->
     <permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE"
-        android:protectionLevel="signature" />
+        android:protectionLevel="signature|setup" />
 
     <!-- Allows applications to set the initial lockscreen state.
          <p>Not for use by third-party applications. @hide -->