Merge "Fix IME test to be compatible with WindowOnBackInvokedDispatcher" into tm-dev am: 024b8dbf1d

Original change: https://googleplex-android-review.googlesource.com/c/platform/cts/+/18868923

Change-Id: Idbea115056eb5edfba72fd3e863520c674c1f9ec
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index d74516b..981f0fa 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -2942,24 +2942,6 @@
                                android.hardware.camera2.CaptureRequest#controlExtendedSceneMode" />
         </activity>
 
-        <activity android:name=".camera.its.CameraMuteToggleActivity"
-                 android:label="@string/camera_hw_toggle_test"
-                 android:exported="true"
-                 android:screenOrientation="landscape">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.cts.intent.category.MANUAL_TEST" />
-            </intent-filter>
-            <meta-data android:name="test_category" android:value="@string/test_category_camera" />
-            <meta-data android:name="test_required_configs" android:value="config_has_camera_toggle"/>
-            <meta-data android:name="test_required_features" android:value="android.hardware.camera.any"/>
-            <meta-data android:name="test_excluded_features"
-                       android:value="android.hardware.type.automotive"/>
-            <meta-data android:name="display_mode"
-                       android:value="single_display_mode" />
-            <meta-data android:name="CddTest" android:value="9.8.13/C-1-3" />
-        </activity>
-
         <activity android:name=".usb.accessory.UsbAccessoryTestActivity"
                 android:label="@string/usb_accessory_test"
                 android:exported="true"
@@ -5616,20 +5598,6 @@
                     android.hardware.usb.UsbManager#requestPermission"/>
         </activity>
 
-        <activity android:name=".audio.AudioMicrophoneMuteToggleActivity"
-                android:label="@string/audio_mic_toggle_test"
-                android:exported="true"
-                android:screenOrientation="locked">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.cts.intent.category.MANUAL_TEST" />
-            </intent-filter>
-            <meta-data android:name="test_category" android:value="@string/test_category_audio" />
-            <meta-data android:name="test_required_configs" android:value="config_has_mic_toggle"/>
-            <meta-data android:name="display_mode" android:value="multi_display_mode" />
-            <meta-data android:name="CddTest" android:value="9.8.13/C-1-3" />
-        </activity>
-
         <service android:name=".tv.MockTvInputService"
                 android:exported="true"
             android:permission="android.permission.BIND_TV_INPUT">
diff --git a/apps/CtsVerifier/res/layout/cam_hw_toggle.xml b/apps/CtsVerifier/res/layout/cam_hw_toggle.xml
deleted file mode 100644
index 95aced3..0000000
--- a/apps/CtsVerifier/res/layout/cam_hw_toggle.xml
+++ /dev/null
@@ -1,100 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!-- Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"
-              style="@style/RootLayoutPadding">
-
-<LinearLayout
-      android:layout_width="match_parent"
-      android:layout_height="match_parent"
-      android:orientation="vertical">
-
-    <LinearLayout
-        android:orientation="horizontal"
-        android:layout_width="fill_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1">
-
-        <LinearLayout
-            android:orientation="vertical"
-            android:layout_width="0dp"
-            android:layout_height="fill_parent"
-            android:layout_weight="3">
-
-            <TextureView
-                android:id="@+id/preview_view"
-                android:layout_height="0dp"
-                android:layout_width="fill_parent"
-                android:layout_weight="3" />
-            <TextView
-                android:id="@+id/preview_label"
-                android:layout_height="wrap_content"
-                android:layout_width="fill_parent"
-                android:padding="2dp"
-                android:textSize="16sp"
-                android:gravity="center" />
-
-        </LinearLayout>
-        <LinearLayout
-            android:orientation="vertical"
-            android:layout_width="0dp"
-            android:layout_height="fill_parent"
-            android:layout_weight="3">
-
-            <ImageView
-                android:id="@+id/image_view"
-                android:layout_height="0dp"
-                android:layout_width="fill_parent"
-                android:layout_weight="3" />
-            <TextView
-                android:id="@+id/image_label"
-                android:layout_height="wrap_content"
-                android:layout_width="fill_parent"
-                android:padding="2dp"
-                android:textSize="16sp"
-                android:gravity="center" />
-
-        </LinearLayout>
-
-        <LinearLayout
-            android:orientation="vertical"
-            android:layout_width="0dp"
-            android:layout_height="fill_parent"
-            android:layout_weight="3"
-            android:gravity="bottom">
-
-            <TextView
-                android:id="@+id/instruction_text"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/camera_hw_toggle_test_instruction" />
-            <Button
-                android:id="@+id/take_picture_button"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:text="@string/co_photo_button_caption" />
-
-        </LinearLayout>
-
-    </LinearLayout>
-
-    <include layout="@layout/pass_fail_buttons" />
-
-</LinearLayout>
-</ScrollView>
diff --git a/apps/CtsVerifier/res/layout/mic_hw_toggle.xml b/apps/CtsVerifier/res/layout/mic_hw_toggle.xml
deleted file mode 100644
index a17abd4..0000000
--- a/apps/CtsVerifier/res/layout/mic_hw_toggle.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"
-              style="@style/RootLayoutPadding">
-
-<LinearLayout
-      android:layout_width="match_parent"
-      android:layout_height="match_parent"
-      android:orientation="vertical">
-
-  <TextView
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:id="@+id/info_text"/>
-
-  <LinearLayout
-      android:layout_width="match_parent"
-      android:layout_height="0dp"
-      android:layout_weight="3"
-      android:orientation="horizontal">
-    <Button
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="5"
-        android:text="@string/hifi_ultrasound_test_record"
-        android:id="@+id/recorder_button"/>
-  </LinearLayout>
-
-      <include layout="@layout/pass_fail_buttons" />
-      </LinearLayout>
-</ScrollView>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index baaa61d..9efb079 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -6658,41 +6658,4 @@
     <string name="report_distance_range_5m_gt_90p">Measurement Range at 5m (90th percentile)</string>
     <string name="nan_precision">Nan Precision Test</string>
     <string name="wifi_nan">WiFi NAN</string>
-
-    <!-- Strings for AudioMicrophoneMuteToggleActivity -->
-    <string name="audio_mic_toggle_test">Audio Microphone Hardware Toggle Mute Test</string>
-    <string name="audio_mic_toggle_test_info">
-        This test verifies that devices which implement microphone hardware privacy toggles enforce sensor privacy when toggles are enabled.
-        \nTo pass the test:
-        \n  - <a href="https://source.android.com/compatibility/android-cdd#9813_sensorprivacymanager">The audio stream should be muted</a>.
-        \n  - A dialog or notification should be shown that informs the user that the sensor privacy is enabled.
-    </string>
-    <string name="audio_mic_toggle_test_instruction1">
-        Mute the microphone using the hardware privacy toggle.
-        \nPress the RECORD button.
-        \nObserve a dialog with information regarding the microphone being blocked
-        \nIgnore/cancel the dialog and wait for the recording to complete.
-        \nThe pass button will be enabled if the test succeeded.</string>
-    <string name="audio_mic_toggle_test_analyzing">Analyzing, please wait...\n</string>
-
-    <!-- Strings for CameraMuteToggleActivity -->
-    <string name="camera_hw_toggle_test">Camera Hardware Toggle Mute Test</string>
-    <string name="camera_hw_toggle_test_info">
-        This test verifies that devices which implement camera hardware privacy toggles enforce sensor privacy when toggles are enabled.
-        \nTo pass the test:
-        \n  - <a href="https://source.android.com/compatibility/android-cdd#9813_sensorprivacymanager">The video stream should be muted</a>.
-        \n  - A dialog or notification should be shown that informs the user that the sensor privacy is enabled.
-    </string>
-    <string name="camera_hw_toggle_test_instruction">
-        Mute the camera using the hardware privacy toggle.
-        \nObserve a dialog with information regarding the camera being blocked.
-        \nCamera preview should show a blank feed.
-        \nPress the Take Photo button.
-        \nCaptured image should be black.
-        \nMark the test as passing if the above conditions are met.</string>
-    <string name="camera_hw_toggle_test_no_camera">
-        No available camera found.
-        \nAdd or enable a camera and re-run this test.
-    </string>
-
 </resources>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
index 9393283..7bf6b5d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
@@ -19,14 +19,12 @@
 import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
 import static com.android.cts.verifier.TestListActivity.sInitialLaunch;
 
-import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
-import android.hardware.SensorPrivacyManager;
 import android.os.Bundle;
 import android.telephony.TelephonyManager;
 import android.util.Log;
@@ -144,10 +142,6 @@
 
     private static final String CONFIG_QUICK_SETTINGS_SUPPORTED = "config_quick_settings_supported";
 
-    private static final String CONFIG_HAS_MIC_TOGGLE = "config_has_mic_toggle";
-
-    private static final String CONFIG_HAS_CAMERA_TOGGLE = "config_has_camera_toggle";
-
     /** The config to represent that a test is only needed to run in the main display mode
      * (i.e. unfolded) */
     private static final String SINGLE_DISPLAY_MODE = "single_display_mode";
@@ -486,10 +480,6 @@
                             return false;
                         }
                         break;
-                    case CONFIG_HAS_MIC_TOGGLE:
-                        return isHardwareToggleSupported(SensorPrivacyManager.Sensors.MICROPHONE);
-                    case CONFIG_HAS_CAMERA_TOGGLE:
-                        return isHardwareToggleSupported(SensorPrivacyManager.Sensors.CAMERA);
                     default:
                         break;
                 }
@@ -591,16 +581,4 @@
             super.loadTestResults();
         }
     }
-
-    @SuppressLint("NewApi")
-    private boolean isHardwareToggleSupported(final int sensorType) {
-        boolean isToggleSupported = false;
-        SensorPrivacyManager sensorPrivacyManager = mContext.getSystemService(
-                SensorPrivacyManager.class);
-        if (sensorPrivacyManager != null) {
-            isToggleSupported = sensorPrivacyManager.supportsSensorToggle(
-                    SensorPrivacyManager.TOGGLE_TYPE_HARDWARE, sensorType);
-        }
-        return isToggleSupported;
-    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioAEC.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioAEC.java
index 6b9bf05..48795ef 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioAEC.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioAEC.java
@@ -17,11 +17,11 @@
 package com.android.cts.verifier.audio;
 
 import android.content.Context;
-import android.media.audiofx.AcousticEchoCanceler;
 import android.media.AudioManager;
-import android.media.AudioTrack;
 import android.media.AudioRecord;
+import android.media.AudioTrack;
 import android.media.MediaRecorder;
+import android.media.audiofx.AcousticEchoCanceler;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
@@ -32,71 +32,130 @@
 
 import com.android.compatibility.common.util.ResultType;
 import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
-import com.android.cts.verifier.audio.soundio.SoundRecorderObject;
-import com.android.cts.verifier.audio.wavelib.*;
 import com.android.cts.verifier.CtsVerifierReportLog;
-import com.android.cts.verifier.PassFailButtons;
 import com.android.cts.verifier.R;
-
-import static com.android.cts.verifier.TestListActivity.sCurrentDisplayMode;
-import static com.android.cts.verifier.TestListAdapter.setTestNameSuffix;
+import com.android.cts.verifier.audio.wavelib.DspBufferDouble;
+import com.android.cts.verifier.audio.wavelib.DspBufferMath;
+import com.android.cts.verifier.audio.wavelib.PipeShort;
 
 public class AudioAEC extends AudioFrequencyActivity implements View.OnClickListener {
     private static final String TAG = "AudioAEC";
 
-    // Test State
+    private static final int TEST_NONE = -1;
     private static final int TEST_AEC = 0;
+    private static final int TEST_COUNT = 1;
+    private static final float MAX_VAL = (float)(1 << 15);
 
-    // UI
+    private int mCurrentTest = TEST_NONE;
     private LinearLayout mLinearLayout;
     private Button mButtonTest;
     private Button mButtonMandatoryYes;
     private Button mButtonMandatoryNo;
     private ProgressBar mProgress;
     private TextView mResultTest;
+    private boolean mTestAECPassed;
+    private SoundPlayerObject mSPlayer;
+    private SoundRecorderObject mSRecorder;
+    private AcousticEchoCanceler mAec;
 
-    // Sound IO
+    private boolean mMandatory = true;
+
     private final int mBlockSizeSamples = 4096;
     private final int mSamplingRate = 48000;
     private final int mSelectedRecordSource = MediaRecorder.AudioSource.VOICE_COMMUNICATION;
 
-    private SoundPlayerObject mSPlayer;
-    private SoundRecorderObject mSRecorder;
-
-    // Test Results
-    private boolean mMandatory = true;
-    private boolean mTestAECPassed;
-
     private final int TEST_DURATION_MS = 8000;
     private final int SHOT_FREQUENCY_MS = 200;
     private final int CORRELATION_DURATION_MS = TEST_DURATION_MS - 3000;
     private final int SHOT_COUNT_CORRELATION = CORRELATION_DURATION_MS/SHOT_FREQUENCY_MS;
     private final int SHOT_COUNT = TEST_DURATION_MS/SHOT_FREQUENCY_MS;
-
     private final float MIN_RMS_DB = -60.0f; //dB
     private final float MIN_RMS_VAL = (float)Math.pow(10,(MIN_RMS_DB/20));
 
     private final double TEST_THRESHOLD_AEC_ON = 0.5;
     private final double TEST_THRESHOLD_AEC_OFF = 0.6;
-    private RmsHelper mRMSRecorder1 =
-            new RmsHelper(mBlockSizeSamples, SHOT_COUNT, MIN_RMS_DB, MIN_RMS_VAL);
-    private RmsHelper mRMSRecorder2 =
-            new RmsHelper(mBlockSizeSamples, SHOT_COUNT, MIN_RMS_DB, MIN_RMS_VAL);
+    private RmsHelper mRMSRecorder1 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
+    private RmsHelper mRMSRecorder2 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
 
-    private RmsHelper mRMSPlayer1 =
-            new RmsHelper(mBlockSizeSamples, SHOT_COUNT, MIN_RMS_DB, MIN_RMS_VAL);
-    private RmsHelper mRMSPlayer2 =
-            new RmsHelper(mBlockSizeSamples, SHOT_COUNT, MIN_RMS_DB, MIN_RMS_VAL);
+    private RmsHelper mRMSPlayer1 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
+    private RmsHelper mRMSPlayer2 = new RmsHelper(mBlockSizeSamples, SHOT_COUNT);
 
     private Thread mTestThread;
 
-    // ReportLog schema
-    private static final String SECTION_AEC = "aec_activity";
-    private static final String KEY_AEC_MANDATORY = "aec_mandatory";
-    private static final String KEY_AEC_MAX_WITH = "max_with_aec";
-    private static final String KEY_AEC_MAX_WITHOUT = "max_without_aec";
-    private static final String KEY_AEC_RESULT = "result_string";
+    //RMS helpers
+    public class RmsHelper {
+        private double mRmsCurrent;
+        public int mBlockSize;
+        private int mShoutCount;
+        public boolean mRunning = false;
+
+        private short[] mAudioShortArray;
+
+        private DspBufferDouble mRmsSnapshots;
+        private int mShotIndex;
+
+        public RmsHelper(int blockSize, int shotCount) {
+            mBlockSize = blockSize;
+            mShoutCount = shotCount;
+            reset();
+        }
+
+        public void reset() {
+            mAudioShortArray = new short[mBlockSize];
+            mRmsSnapshots = new DspBufferDouble(mShoutCount);
+            mShotIndex = 0;
+            mRmsCurrent = 0;
+            mRunning = false;
+        }
+
+        public void captureShot() {
+            if (mShotIndex >= 0 && mShotIndex < mRmsSnapshots.getSize()) {
+                mRmsSnapshots.setValue(mShotIndex++, mRmsCurrent);
+            }
+        }
+
+        public void setRunning(boolean running) {
+            mRunning = running;
+        }
+
+        public double getRmsCurrent() {
+            return mRmsCurrent;
+        }
+
+        public DspBufferDouble getRmsSnapshots() {
+            return mRmsSnapshots;
+        }
+
+        public boolean updateRms(PipeShort pipe, int channelCount, int channel) {
+            if (mRunning) {
+                int samplesAvailable = pipe.availableToRead();
+                while (samplesAvailable >= mBlockSize) {
+                    pipe.read(mAudioShortArray, 0, mBlockSize);
+
+                    double rmsTempSum = 0;
+                    int count = 0;
+                    for (int i = channel; i < mBlockSize; i += channelCount) {
+                        float value = mAudioShortArray[i] / MAX_VAL;
+
+                        rmsTempSum += value * value;
+                        count++;
+                    }
+                    float rms = count > 0 ? (float)Math.sqrt(rmsTempSum / count) : 0f;
+                    if (rms < MIN_RMS_VAL) {
+                        rms = MIN_RMS_VAL;
+                    }
+
+                    double alpha = 0.9;
+                    double total_rms = rms * alpha + mRmsCurrent * (1.0f - alpha);
+                    mRmsCurrent = total_rms;
+
+                    samplesAvailable = pipe.availableToRead();
+                }
+                return true;
+            }
+            return false;
+        }
+    }
 
     //compute Acoustic Coupling Factor
     private double computeAcousticCouplingFactor(DspBufferDouble buffRmsPlayer,
@@ -143,7 +202,7 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.audio_aec_activity);
 
-        // "AEC Mandatory" Buttons
+        //
         mLinearLayout = (LinearLayout)findViewById(R.id.audio_aec_test_layout);
         mButtonMandatoryYes = (Button) findViewById(R.id.audio_aec_mandatory_yes);
         mButtonMandatoryYes.setOnClickListener(this);
@@ -151,15 +210,16 @@
         mButtonMandatoryNo.setOnClickListener(this);
         enableUILayout(mLinearLayout, false);
 
-        // Test Button
+        // Test
         mButtonTest = (Button) findViewById(R.id.audio_aec_button_test);
         mButtonTest.setOnClickListener(this);
         mProgress = (ProgressBar) findViewById(R.id.audio_aec_test_progress_bar);
         mResultTest = (TextView) findViewById(R.id.audio_aec_test_result);
 
-        showProgressIndicator(false);
+        showView(mProgress, false);
 
         mSPlayer = new SoundPlayerObject(false, mBlockSizeSamples) {
+
             @Override
             public void periodicNotification(AudioTrack track) {
                 int channelCount = getChannelCount();
@@ -179,11 +239,12 @@
 
         setPassFailButtonClickListeners();
         getPassButton().setEnabled(false);
-        setInfoResources(R.string.audio_aec_test, R.string.audio_aec_info, -1);
+        setInfoResources(R.string.audio_aec_test,
+                R.string.audio_aec_info, -1);
     }
 
-    private void showProgressIndicator(boolean show) {
-        mProgress.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
+    private void showView(View v, boolean show) {
+        v.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
     }
 
     @Override
@@ -213,11 +274,7 @@
             Log.v(TAG,"test Thread already running.");
             return;
         }
-
         mTestThread = new Thread(new AudioTestRunner(TAG, TEST_AEC, mMessageHandler) {
-            // AcousticEchoCanceler
-            private AcousticEchoCanceler mAec;
-
             public void run() {
                 super.run();
 
@@ -433,39 +490,28 @@
                                   String msg) {
 
         CtsVerifierReportLog reportLog = getReportLog();
-        reportLog.addValue(KEY_AEC_MANDATORY,
+        reportLog.addValue("AEC_mandatory",
                 aecMandatory,
                 ResultType.NEUTRAL,
                 ResultUnit.NONE);
 
-        reportLog.addValue(KEY_AEC_MAX_WITH,
+        reportLog.addValue("max_with_AEC",
                 maxAEC,
                 ResultType.LOWER_BETTER,
                 ResultUnit.SCORE);
 
-        reportLog.addValue(KEY_AEC_MAX_WITHOUT,
+        reportLog.addValue("max_without_AEC",
                 maxNoAEC,
                 ResultType.HIGHER_BETTER,
                 ResultUnit.SCORE);
 
-        reportLog.addValue(KEY_AEC_RESULT,
+        reportLog.addValue("result_string",
                 msg,
                 ResultType.NEUTRAL,
                 ResultUnit.NONE);
     }
 
-    //
-    // PassFailButtons Overrides
-    //
-    @Override
-    public String getReportFileName() { return PassFailButtons.AUDIO_TESTS_REPORT_LOG_NAME; }
-
-    @Override
-    public final String getReportSectionName() {
-        return setTestNameSuffix(sCurrentDisplayMode, SECTION_AEC);
-    }
-
-    @Override
+    @Override // PassFailButtons
     public void recordTestResults() {
         getReportLog().submit();
     }
@@ -477,7 +523,7 @@
         public void testStarted(int testId, String str) {
             super.testStarted(testId, str);
             Log.v(TAG, "Test Started! " + testId + " str:"+str);
-            showProgressIndicator(true);
+            showView(mProgress, true);
             mTestAECPassed = false;
             getPassButton().setEnabled(false);
             mResultTest.setText("test in progress..");
@@ -494,7 +540,7 @@
         public void testEndedOk(int testId, String str) {
             super.testEndedOk(testId, str);
             Log.v(TAG, "Test EndedOk. " + testId + " str:"+str);
-            showProgressIndicator(false);
+            showView(mProgress, false);
             mResultTest.setText("test completed. " + str);
             if (mTestAECPassed) {
                 getPassButton().setEnabled(true);;
@@ -505,7 +551,7 @@
         public void testEndedError(int testId, String str) {
             super.testEndedError(testId, str);
             Log.v(TAG, "Test EndedError. " + testId + " str:"+str);
-            showProgressIndicator(false);
+            showView(mProgress, false);
             mResultTest.setText("test failed. " + str);
         }
     };
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java
index 9b24039..582ea0c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyLineActivity.java
@@ -32,7 +32,6 @@
 
 import com.android.compatibility.common.util.ResultType;
 import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
 import com.android.cts.verifier.CtsVerifierReportLog;
 import com.android.cts.verifier.R;
 import com.android.cts.verifier.audio.wavelib.DspBufferComplex;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyMicActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyMicActivity.java
index e066943..a83709d 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyMicActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyMicActivity.java
@@ -40,9 +40,6 @@
 import com.android.cts.verifier.audio.wavelib.DspFftServer;
 import com.android.cts.verifier.audio.wavelib.DspWindow;
 import com.android.cts.verifier.audio.wavelib.PipeShort;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
-import com.android.cts.verifier.audio.wavelib.*;
-
 import com.android.cts.verifier.audio.wavelib.VectorAverage;
 
 /**
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencySpeakerActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencySpeakerActivity.java
index 0e63b6f..301eb9c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencySpeakerActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencySpeakerActivity.java
@@ -32,7 +32,6 @@
 
 import com.android.compatibility.common.util.ResultType;
 import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
 import com.android.cts.verifier.CtsVerifierReportLog;
 import com.android.cts.verifier.R;
 import com.android.cts.verifier.audio.wavelib.DspBufferComplex;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyUnprocessedActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyUnprocessedActivity.java
index 21de117..3819be2 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyUnprocessedActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyUnprocessedActivity.java
@@ -34,7 +34,6 @@
 import com.android.compatibility.common.util.CddTest;
 import com.android.compatibility.common.util.ResultType;
 import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
 import com.android.cts.verifier.CtsVerifierReportLog;
 import com.android.cts.verifier.R;
 import com.android.cts.verifier.audio.wavelib.DspBufferComplex;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyVoiceRecognitionActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyVoiceRecognitionActivity.java
index 442f626..23a016a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyVoiceRecognitionActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioFrequencyVoiceRecognitionActivity.java
@@ -30,8 +30,6 @@
 
 import com.android.compatibility.common.util.ResultType;
 import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.audio.soundio.SoundPlayerObject;
-import com.android.cts.verifier.audio.soundio.SoundRecorderObject;
 import com.android.cts.verifier.CtsVerifierReportLog;
 import com.android.cts.verifier.R;
 import com.android.cts.verifier.audio.wavelib.DspBufferComplex;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioMicrophoneMuteToggleActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioMicrophoneMuteToggleActivity.java
deleted file mode 100644
index 1e250ef..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioMicrophoneMuteToggleActivity.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.cts.verifier.audio;
-
-import android.media.MediaRecorder;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.text.method.ScrollingMovementMethod;
-import android.util.Log;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-
-import com.android.compatibility.common.util.ResultType;
-import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.CtsVerifierReportLog;
-import com.android.cts.verifier.PassFailButtons;
-import com.android.cts.verifier.R;
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.wavelib.WavAnalyzer;
-
-/**
- * Test for manual verification of microphone privacy hardware switches
- */
-public class AudioMicrophoneMuteToggleActivity extends PassFailButtons.Activity {
-
-    public enum Status {
-        START, RECORDING, DONE, PLAYER
-    }
-
-    private static final String TAG = "AudioMicrophoneMuteToggleActivity";
-
-    private Status mStatus = Status.START;
-    private TextView mInfoText;
-    private Button mRecorderButton;
-
-    private int mAudioSource = -1;
-    private int mRecordRate = 0;
-
-    // keys for report log
-    private static final String KEY_REC_RATE = "rec_rate";
-    private static final String KEY_AUDIO_SOURCE = "audio_source";
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.mic_hw_toggle);
-        setInfoResources(R.string.audio_mic_toggle_test, R.string.audio_mic_toggle_test_info, -1);
-        setPassFailButtonClickListeners();
-        getPassButton().setEnabled(false);
-
-        mInfoText = findViewById(R.id.info_text);
-        mInfoText.setMovementMethod(new ScrollingMovementMethod());
-        mInfoText.setText(R.string.audio_mic_toggle_test_instruction1);
-
-        mRecorderButton = findViewById(R.id.recorder_button);
-        mRecorderButton.setEnabled(true);
-
-        final AudioRecordHelper audioRecorder = AudioRecordHelper.getInstance();
-        mRecordRate = audioRecorder.getSampleRate();
-
-        mRecorderButton.setOnClickListener(new View.OnClickListener() {
-            private WavAnalyzerTask mWavAnalyzerTask = null;
-
-            private void stopRecording() {
-                audioRecorder.stop();
-                mInfoText.append(getString(R.string.audio_mic_toggle_test_analyzing));
-                mWavAnalyzerTask = new WavAnalyzerTask(audioRecorder.getByte());
-                mWavAnalyzerTask.execute();
-                mStatus = Status.DONE;
-            }
-
-            @Override
-            public void onClick(View v) {
-                switch (mStatus) {
-                    case START:
-                        mInfoText.append("Recording at " + mRecordRate + "Hz using ");
-                        mAudioSource = audioRecorder.getAudioSource();
-                        switch (mAudioSource) {
-                            case MediaRecorder.AudioSource.MIC:
-                                mInfoText.append("MIC");
-                                break;
-                            case MediaRecorder.AudioSource.VOICE_RECOGNITION:
-                                mInfoText.append("VOICE_RECOGNITION");
-                                break;
-                            default:
-                                mInfoText.append("UNEXPECTED " + mAudioSource);
-                                break;
-                        }
-                        mInfoText.append("\n");
-                        mStatus = Status.RECORDING;
-
-                        mRecorderButton.setEnabled(false);
-                        audioRecorder.start();
-
-                        final View finalV = v;
-                        new Thread() {
-                            @Override
-                            public void run() {
-                                double recordingDuration_millis = (1000 * (2.5
-                                        + AudioCommon.PREFIX_LENGTH_S
-                                        + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S
-                                        + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S
-                                        + AudioCommon.PIP_NUM * (AudioCommon.PIP_DURATION_S
-                                        + AudioCommon.PAUSE_DURATION_S)
-                                        * AudioCommon.REPETITIONS));
-                                Log.d(TAG, "Recording for " + recordingDuration_millis + "ms");
-                                try {
-                                    Thread.sleep((long) recordingDuration_millis);
-                                } catch (InterruptedException e) {
-                                    throw new RuntimeException(e);
-                                }
-                                runOnUiThread(new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        stopRecording();
-                                    }
-                                });
-                            }
-                        }.start();
-
-                        break;
-
-                    default:
-                        break;
-                }
-            }
-        });
-
-    }
-
-    @Override
-    public void recordTestResults() {
-        CtsVerifierReportLog reportLog = getReportLog();
-
-        reportLog.addValue(
-                KEY_REC_RATE,
-                mRecordRate,
-                ResultType.NEUTRAL,
-                ResultUnit.NONE);
-
-        reportLog.addValue(
-                KEY_AUDIO_SOURCE,
-                mAudioSource,
-                ResultType.NEUTRAL,
-                ResultUnit.NONE);
-
-        reportLog.submit();
-    }
-
-    /**
-     * AsyncTask class for the analyzing.
-     */
-    private class WavAnalyzerTask extends AsyncTask<Void, String, String>
-            implements WavAnalyzer.Listener {
-
-        private static final String TAG = "WavAnalyzerTask";
-        private final WavAnalyzer mWavAnalyzer;
-
-        public WavAnalyzerTask(byte[] recording) {
-            mWavAnalyzer = new WavAnalyzer(recording, AudioCommon.RECORDING_SAMPLE_RATE_HZ,
-                    WavAnalyzerTask.this);
-        }
-
-        @Override
-        protected String doInBackground(Void... params) {
-            boolean result = mWavAnalyzer.doWork();
-            if (result) {
-                return getString(R.string.pass_button_text);
-            }
-            return getString(R.string.fail_button_text);
-        }
-
-        @Override
-        protected void onPostExecute(String result) {
-            if (mWavAnalyzer.isSilence()) {
-                mInfoText.append(getString(R.string.passed));
-                getPassButton().setEnabled(true);
-            } else {
-                mInfoText.append(getString(R.string.failed));
-            }
-        }
-
-        @Override
-        protected void onProgressUpdate(String... values) {
-            for (String message : values) {
-                Log.d(TAG, message);
-            }
-        }
-
-        @Override
-        public void sendMessage(String message) {
-            publishProgress(message);
-        }
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioCommon.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/Common.java
similarity index 90%
rename from apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioCommon.java
rename to apps/CtsVerifier/src/com/android/cts/verifier/audio/Common.java
index ba5e39b2..df7460a 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/audiolib/AudioCommon.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/Common.java
@@ -1,18 +1,15 @@
-package com.android.cts.verifier.audio.audiolib;
+package com.android.cts.verifier.audio;
 
 import android.media.AudioManager;
 import android.media.AudioTrack;
 
-import com.android.cts.verifier.audio.AudioRecordHelper;
-import com.android.cts.verifier.audio.Util;
-
 import java.util.ArrayList;
 import java.util.Random;
 
 /**
  * This class stores common constants and methods.
  */
-public class AudioCommon {
+public class Common {
 
   public static final int RECORDING_SAMPLE_RATE_HZ
       = AudioRecordHelper.getInstance().getSampleRate();
@@ -98,7 +95,7 @@
   private static double[] frequencies() {
     double[] originalFrequencies = originalFrequencies();
 
-    double[] randomFrequencies = new double[AudioCommon.REPETITIONS * originalFrequencies.length];
+    double[] randomFrequencies = new double[Common.REPETITIONS * originalFrequencies.length];
     for (int i = 0; i < REPETITIONS * originalFrequencies.length; i++) {
       randomFrequencies[i] = originalFrequencies[ORDER[i] % originalFrequencies.length];
     }
@@ -111,13 +108,13 @@
    */
   private static double[] originalFrequencies() {
     ArrayList<Double> frequencies = new ArrayList<Double>();
-    double frequency = AudioCommon.MIN_FREQUENCY_HZ;
-    while (frequency <= AudioCommon.MAX_FREQUENCY_HZ) {
+    double frequency = Common.MIN_FREQUENCY_HZ;
+    while (frequency <= Common.MAX_FREQUENCY_HZ) {
       frequencies.add(new Double(frequency));
       if ((frequency >= 18500) && (frequency < 20000)) {
-        frequency += AudioCommon.FREQUENCY_STEP_HZ;
+        frequency += Common.FREQUENCY_STEP_HZ;
       } else {
-        frequency += AudioCommon.FREQUENCY_STEP_HZ * 10;
+        frequency += Common.FREQUENCY_STEP_HZ * 10;
       }
     }
     Double[] frequenciesArray = frequencies.toArray(new Double[frequencies.size()]);
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundSpeakerTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundSpeakerTestActivity.java
index b071652..3dccf1f1 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundSpeakerTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundSpeakerTestActivity.java
@@ -37,10 +37,6 @@
 import android.widget.TextView;
 import java.util.Arrays;
 
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.soundio.SoundGenerator;
-import com.android.cts.verifier.audio.wavelib.WavAnalyzer;
-
 import com.androidplot.xy.PointLabelFormatter;
 import com.androidplot.xy.LineAndPointFormatter;
 import com.androidplot.xy.SimpleXYSeries;
@@ -185,12 +181,11 @@
               @Override
               public void run() {
                 Double recordingDuration_millis = new Double(1000 * (2.5
-                    + AudioCommon.PREFIX_LENGTH_S
-                    + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S
-                    + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S
-                    + AudioCommon.PIP_NUM
-                        * (AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S)
-                    * AudioCommon.REPETITIONS));
+                    + Common.PREFIX_LENGTH_S
+                    + Common.PAUSE_BEFORE_PREFIX_DURATION_S
+                    + Common.PAUSE_AFTER_PREFIX_DURATION_S
+                    + Common.PIP_NUM * (Common.PIP_DURATION_S + Common.PAUSE_DURATION_S)
+                    * Common.REPETITIONS));
                 Log.d(TAG, "Recording for " + recordingDuration_millis + "ms");
                 try {
                   Thread.sleep(recordingDuration_millis.intValue());
@@ -267,17 +262,17 @@
     XYPlot plot = (XYPlot) popupView.findViewById(R.id.responseChart);
     plot.setDomainStep(XYStepMode.INCREMENT_BY_VAL, 2000);
 
-    Double[] frequencies = new Double[AudioCommon.PIP_NUM];
-    for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
-      frequencies[i] = new Double(AudioCommon.FREQUENCIES_ORIGINAL[i]);
+    Double[] frequencies = new Double[Common.PIP_NUM];
+    for (int i = 0; i < Common.PIP_NUM; i++) {
+      frequencies[i] = new Double(Common.FREQUENCIES_ORIGINAL[i]);
     }
 
     if (wavAnalyzerTask != null) {
 
       double[][] power = wavAnalyzerTask.getPower();
-      for(int i = 0; i < AudioCommon.REPETITIONS; i++) {
-        Double[] powerWrap = new Double[AudioCommon.PIP_NUM];
-        for (int j = 0; j < AudioCommon.PIP_NUM; j++) {
+      for(int i = 0; i < Common.REPETITIONS; i++) {
+        Double[] powerWrap = new Double[Common.PIP_NUM];
+        for (int j = 0; j < Common.PIP_NUM; j++) {
           powerWrap[j] = new Double(10 * Math.log10(power[j][i]));
         }
         XYSeries series = new SimpleXYSeries(
@@ -292,8 +287,8 @@
       }
 
       double[] noiseDB = wavAnalyzerTask.getNoiseDB();
-      Double[] noiseDBWrap = new Double[AudioCommon.PIP_NUM];
-      for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+      Double[] noiseDBWrap = new Double[Common.PIP_NUM];
+      for (int i = 0; i < Common.PIP_NUM; i++) {
         noiseDBWrap[i] = new Double(noiseDB[i]);
       }
 
@@ -308,8 +303,8 @@
       plot.addSeries(noiseSeries, noiseSeriesFormat);
 
       double[] dB = wavAnalyzerTask.getDB();
-      Double[] dBWrap = new Double[AudioCommon.PIP_NUM];
-      for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+      Double[] dBWrap = new Double[Common.PIP_NUM];
+      for (int i = 0; i < Common.PIP_NUM; i++) {
         dBWrap[i] = new Double(dB[i]);
       }
 
@@ -323,7 +318,7 @@
           R.xml.ultrasound_line_formatter_median);
       plot.addSeries(series, seriesFormat);
 
-      Double[] passX = new Double[] {AudioCommon.MIN_FREQUENCY_HZ, AudioCommon.MAX_FREQUENCY_HZ};
+      Double[] passX = new Double[] {Common.MIN_FREQUENCY_HZ, Common.MAX_FREQUENCY_HZ};
       Double[] passY = new Double[] {wavAnalyzerTask.getThreshold(), wavAnalyzerTask.getThreshold()};
       XYSeries passSeries = new SimpleXYSeries(
           Arrays.asList(passX), Arrays.asList(passY), "passing");
@@ -339,7 +334,7 @@
    * Plays the generated pips.
    */
   private void play() {
-    play(SoundGenerator.getInstance().getByte(), AudioCommon.PLAYING_SAMPLE_RATE_HZ);
+    play(SoundGenerator.getInstance().getByte(), Common.PLAYING_SAMPLE_RATE_HZ);
   }
 
   /**
@@ -369,7 +364,7 @@
     WavAnalyzer wavAnalyzer;
 
     public WavAnalyzerTask(byte[] recording) {
-      wavAnalyzer = new WavAnalyzer(recording, AudioCommon.RECORDING_SAMPLE_RATE_HZ,
+      wavAnalyzer = new WavAnalyzer(recording, Common.RECORDING_SAMPLE_RATE_HZ,
           WavAnalyzerTask.this);
     }
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundTestActivity.java
index 2948e38..f5e4271 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/HifiUltrasoundTestActivity.java
@@ -37,10 +37,6 @@
 import android.widget.TextView;
 import java.util.Arrays;
 
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.soundio.SoundGenerator;
-import com.android.cts.verifier.audio.wavelib.WavAnalyzer;
-
 import com.androidplot.xy.PointLabelFormatter;
 import com.androidplot.xy.LineAndPointFormatter;
 import com.androidplot.xy.SimpleXYSeries;
@@ -165,11 +161,11 @@
               @Override
               public void run() {
                 Double recordingDuration_millis = new Double(1000 * (2.5
-                    + AudioCommon.PREFIX_LENGTH_S
-                    + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S
-                    + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S
-                    + AudioCommon.PIP_NUM * (AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S)
-                    * AudioCommon.REPETITIONS));
+                    + Common.PREFIX_LENGTH_S
+                    + Common.PAUSE_BEFORE_PREFIX_DURATION_S
+                    + Common.PAUSE_AFTER_PREFIX_DURATION_S
+                    + Common.PIP_NUM * (Common.PIP_DURATION_S + Common.PAUSE_DURATION_S)
+                    * Common.REPETITIONS));
                 Log.d(TAG, "Recording for " + recordingDuration_millis + "ms");
                 try {
                   Thread.sleep(recordingDuration_millis.intValue());
@@ -225,18 +221,18 @@
     XYPlot plot = (XYPlot) popupView.findViewById(R.id.responseChart);
     plot.setDomainStep(XYStepMode.INCREMENT_BY_VAL, 2000);
 
-    Double[] frequencies = new Double[AudioCommon.PIP_NUM];
-    for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
-      frequencies[i] = new Double(AudioCommon.FREQUENCIES_ORIGINAL[i]);
+    Double[] frequencies = new Double[Common.PIP_NUM];
+    for (int i = 0; i < Common.PIP_NUM; i++) {
+      frequencies[i] = new Double(Common.FREQUENCIES_ORIGINAL[i]);
     }
 
     if (wavAnalyzerTask != null && wavAnalyzerTask.getPower() != null &&
         wavAnalyzerTask.getNoiseDB() != null && wavAnalyzerTask.getDB() != null) {
 
       double[][] power = wavAnalyzerTask.getPower();
-      for(int i = 0; i < AudioCommon.REPETITIONS; i++) {
-        Double[] powerWrap = new Double[AudioCommon.PIP_NUM];
-        for (int j = 0; j < AudioCommon.PIP_NUM; j++) {
+      for(int i = 0; i < Common.REPETITIONS; i++) {
+        Double[] powerWrap = new Double[Common.PIP_NUM];
+        for (int j = 0; j < Common.PIP_NUM; j++) {
           powerWrap[j] = new Double(10 * Math.log10(power[j][i]));
         }
         XYSeries series = new SimpleXYSeries(
@@ -251,8 +247,8 @@
       }
 
       double[] noiseDB = wavAnalyzerTask.getNoiseDB();
-      Double[] noiseDBWrap = new Double[AudioCommon.PIP_NUM];
-      for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+      Double[] noiseDBWrap = new Double[Common.PIP_NUM];
+      for (int i = 0; i < Common.PIP_NUM; i++) {
         noiseDBWrap[i] = new Double(noiseDB[i]);
       }
 
@@ -267,8 +263,8 @@
       plot.addSeries(noiseSeries, noiseSeriesFormat);
 
       double[] dB = wavAnalyzerTask.getDB();
-      Double[] dBWrap = new Double[AudioCommon.PIP_NUM];
-      for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+      Double[] dBWrap = new Double[Common.PIP_NUM];
+      for (int i = 0; i < Common.PIP_NUM; i++) {
         dBWrap[i] = new Double(dB[i]);
       }
 
@@ -282,7 +278,7 @@
           R.xml.ultrasound_line_formatter_median);
       plot.addSeries(series, seriesFormat);
 
-      Double[] passX = new Double[] {AudioCommon.MIN_FREQUENCY_HZ, AudioCommon.MAX_FREQUENCY_HZ};
+      Double[] passX = new Double[] {Common.MIN_FREQUENCY_HZ, Common.MAX_FREQUENCY_HZ};
       Double[] passY = new Double[] {wavAnalyzerTask.getThreshold(), wavAnalyzerTask.getThreshold()};
       XYSeries passSeries = new SimpleXYSeries(
           Arrays.asList(passX), Arrays.asList(passY), "passing");
@@ -298,7 +294,7 @@
    * Plays the generated pips.
    */
   private void play() {
-    play(SoundGenerator.getInstance().getByte(), AudioCommon.PLAYING_SAMPLE_RATE_HZ);
+    play(SoundGenerator.getInstance().getByte(), Common.PLAYING_SAMPLE_RATE_HZ);
   }
 
   /**
@@ -328,7 +324,7 @@
     WavAnalyzer wavAnalyzer;
 
     public WavAnalyzerTask(byte[] recording) {
-      wavAnalyzer = new WavAnalyzer(recording, AudioCommon.RECORDING_SAMPLE_RATE_HZ,
+      wavAnalyzer = new WavAnalyzer(recording, Common.RECORDING_SAMPLE_RATE_HZ,
           WavAnalyzerTask.this);
     }
 
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundGenerator.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundGenerator.java
new file mode 100644
index 0000000..0ad9371
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundGenerator.java
@@ -0,0 +1,81 @@
+package com.android.cts.verifier.audio;
+
+/**
+ * Sound generator.
+ */
+public class SoundGenerator {
+
+  private static SoundGenerator instance;
+
+  private final byte[] generatedSound;
+  private final double[] sample;
+
+  private SoundGenerator() {
+    // Initialize sample.
+    int pipNum = Common.PIP_NUM;
+    int prefixTotalLength = Util.toLength(Common.PREFIX_LENGTH_S, Common.PLAYING_SAMPLE_RATE_HZ)
+        + Util.toLength(Common.PAUSE_BEFORE_PREFIX_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ)
+        + Util.toLength(Common.PAUSE_AFTER_PREFIX_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ);
+    int repetitionLength = pipNum * Util.toLength(
+        Common.PIP_DURATION_S + Common.PAUSE_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ);
+    int sampleLength = prefixTotalLength + Common.REPETITIONS * repetitionLength;
+    sample = new double[sampleLength];
+
+    // Fill sample with prefix.
+    System.arraycopy(Common.PREFIX_FOR_PLAYER, 0, sample,
+        Util.toLength(Common.PAUSE_BEFORE_PREFIX_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ),
+        Common.PREFIX_FOR_PLAYER.length);
+
+    // Fill the sample.
+    for (int i = 0; i < pipNum * Common.REPETITIONS; i++) {
+      double[] pip = getPip(Common.WINDOW_FOR_PLAYER, Common.FREQUENCIES[i]);
+      System.arraycopy(pip, 0, sample,
+          prefixTotalLength + i * Util.toLength(
+              Common.PIP_DURATION_S + Common.PAUSE_DURATION_S, Common.PLAYING_SAMPLE_RATE_HZ),
+          pip.length);
+    }
+
+    // Convert sample to byte.
+    generatedSound = new byte[2 * sample.length];
+    int i = 0;
+    for (double dVal : sample) {
+      short val = (short) ((dVal * 32767));
+      generatedSound[i++] = (byte) (val & 0x00ff);
+      generatedSound[i++] = (byte) ((val & 0xff00) >>> 8);
+    }
+  }
+
+  public static SoundGenerator getInstance() {
+    if (instance == null) {
+      instance = new SoundGenerator();
+    }
+    return instance;
+  }
+
+  /**
+   * Gets a pip sample.
+   */
+  private static double[] getPip(double[] window, double frequency) {
+    int pipArrayLength = window.length;
+    double[] pipArray = new double[pipArrayLength];
+    double radPerSample = 2 * Math.PI / (Common.PLAYING_SAMPLE_RATE_HZ / frequency);
+    for (int i = 0; i < pipArrayLength; i++) {
+      pipArray[i] = window[i] * Math.sin(i * radPerSample);
+    }
+    return pipArray;
+  }
+
+  /**
+   * Get generated sound in byte[].
+   */
+  public byte[] getByte() {
+    return generatedSound;
+  }
+
+  /**
+   * Get sample in double[].
+   */
+  public double[] getSample() {
+    return sample;
+  }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundPlayerObject.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundPlayerObject.java
similarity index 99%
rename from apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundPlayerObject.java
rename to apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundPlayerObject.java
index 156460b..0d93dbb 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundPlayerObject.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundPlayerObject.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.cts.verifier.audio.soundio;
+package com.android.cts.verifier.audio;
 
 import android.content.Context;
 import android.media.AudioAttributes;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundRecorderObject.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundRecorderObject.java
similarity index 98%
rename from apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundRecorderObject.java
rename to apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundRecorderObject.java
index d83a5dd..8950ec5 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundRecorderObject.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/SoundRecorderObject.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.cts.verifier.audio.soundio;
+package com.android.cts.verifier.audio;
 
 import android.media.AudioFormat;
 import android.media.AudioRecord;
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/WavAnalyzer.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/WavAnalyzer.java
similarity index 62%
rename from apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/WavAnalyzer.java
rename to apps/CtsVerifier/src/com/android/cts/verifier/audio/WavAnalyzer.java
index 038f080..b75c40b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/WavAnalyzer.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/WavAnalyzer.java
@@ -1,22 +1,4 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.cts.verifier.audio.wavelib;
-
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.Util;
+package com.android.cts.verifier.audio;
 
 import org.apache.commons.math.complex.Complex;
 
@@ -27,8 +9,6 @@
  * Class contains the analysis to calculate frequency response.
  */
 public class WavAnalyzer {
-  final double SILENCE_THRESHOLD = Short.MAX_VALUE / 100.0f;
-
   private final Listener listener;
   private final int sampleRate;  // Recording sampling rate.
   private double[] data;  // Whole recording data.
@@ -80,7 +60,7 @@
   /**
    * Check if the recording is clipped.
    */
-  public boolean isClipped() {
+  boolean isClipped() {
     for (int i = 1; i < data.length; i++) {
       if ((Math.abs(data[i]) >= Short.MAX_VALUE) && (Math.abs(data[i - 1]) >= Short.MAX_VALUE)) {
         listener.sendMessage("WARNING: Data is clipped."
@@ -94,11 +74,11 @@
   /**
    * Check if the result is consistant across trials.
    */
-  public boolean isConsistent() {
-    double[] coeffOfVar = new double[AudioCommon.PIP_NUM];
-    for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
-      double[] powerAtFreq = new double[AudioCommon.REPETITIONS];
-      for (int j = 0; j < AudioCommon.REPETITIONS; j++) {
+  boolean isConsistent() {
+    double[] coeffOfVar = new double[Common.PIP_NUM];
+    for (int i = 0; i < Common.PIP_NUM; i++) {
+      double[] powerAtFreq = new double[Common.REPETITIONS];
+      for (int j = 0; j < Common.REPETITIONS; j++) {
         powerAtFreq[j] = power[i][j];
       }
       coeffOfVar[i] = Util.std(powerAtFreq) / Util.mean(powerAtFreq);
@@ -114,7 +94,7 @@
   /**
    * Determine test pass/fail using the frequency response. Package visible for unit testing.
    */
-  public boolean responsePassesHifiTest(double[] dB) {
+  boolean responsePassesHifiTest(double[] dB) {
     for (int i = 0; i < dB.length; i++) {
       // Precautionary; NaN should not happen.
       if (Double.isNaN(dB[i])) {
@@ -124,16 +104,16 @@
       }
     }
 
-    if (Util.mean(dB) - Util.mean(noiseDB) < AudioCommon.SIGNAL_MIN_STRENGTH_DB_ABOVE_NOISE) {
+    if (Util.mean(dB) - Util.mean(noiseDB) < Common.SIGNAL_MIN_STRENGTH_DB_ABOVE_NOISE) {
       listener.sendMessage("WARNING: Signal is too weak or background noise is too strong."
           + " Turn up the volume of the playback device or move to a quieter location.\n");
       return false;
     }
 
-    int indexOf2000Hz = Util.findClosest(AudioCommon.FREQUENCIES_ORIGINAL, 2000.0);
-    threshold = dB[indexOf2000Hz] + AudioCommon.PASSING_THRESHOLD_DB;
-    int indexOf18500Hz = Util.findClosest(AudioCommon.FREQUENCIES_ORIGINAL, 18500.0);
-    int indexOf20000Hz = Util.findClosest(AudioCommon.FREQUENCIES_ORIGINAL, 20000.0);
+    int indexOf2000Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 2000.0);
+    threshold = dB[indexOf2000Hz] + Common.PASSING_THRESHOLD_DB;
+    int indexOf18500Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 18500.0);
+    int indexOf20000Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 20000.0);
     double[] responseInRange = new double[indexOf20000Hz - indexOf18500Hz];
     System.arraycopy(dB, indexOf18500Hz, responseInRange, 0, responseInRange.length);
     if (Util.mean(responseInRange) < threshold) {
@@ -148,15 +128,15 @@
    * Calculate the Fourier Coefficient at the pip frequency to calculate the frequency response.
    * Package visible for unit testing.
    */
-  public double[] measurePipStrength() {
+  double[] measurePipStrength() {
     listener.sendMessage("Aligning data... Please wait...\n");
     final int dataStartI = alignData();
     final int prefixTotalLength = dataStartI
-        + Util.toLength(AudioCommon.PREFIX_LENGTH_S + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S, sampleRate);
+        + Util.toLength(Common.PREFIX_LENGTH_S + Common.PAUSE_AFTER_PREFIX_DURATION_S, sampleRate);
     listener.sendMessage("Done.\n");
     listener.sendMessage("Prefix starts at " + (double) dataStartI / sampleRate + " s \n");
-    if (dataStartI > Math.round(sampleRate * (AudioCommon.PREFIX_LENGTH_S
-            + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S))) {
+    if (dataStartI > Math.round(sampleRate * (Common.PREFIX_LENGTH_S
+            + Common.PAUSE_BEFORE_PREFIX_DURATION_S + Common.PAUSE_AFTER_PREFIX_DURATION_S))) {
       listener.sendMessage("WARNING: Unexpected prefix start time. May have missed the prefix.\n"
           + "PLAY button should be pressed on the playback device within one second"
           + " after RECORD is pressed on the recording device.\n"
@@ -165,17 +145,17 @@
     }
 
     listener.sendMessage("Analyzing noise strength... Please wait...\n");
-    noisePower = new double[AudioCommon.PIP_NUM][AudioCommon.NOISE_SAMPLES];
-    noiseDB = new double[AudioCommon.PIP_NUM];
-    for (int s = 0; s < AudioCommon.NOISE_SAMPLES; s++) {
-      double[] noisePoints = new double[AudioCommon.WINDOW_FOR_RECORDER.length];
+    noisePower = new double[Common.PIP_NUM][Common.NOISE_SAMPLES];
+    noiseDB = new double[Common.PIP_NUM];
+    for (int s = 0; s < Common.NOISE_SAMPLES; s++) {
+      double[] noisePoints = new double[Common.WINDOW_FOR_RECORDER.length];
       System.arraycopy(data, dataStartI - (s + 1) * noisePoints.length - 1,
           noisePoints, 0, noisePoints.length);
       for (int j = 0; j < noisePoints.length; j++) {
-        noisePoints[j] = noisePoints[j] * AudioCommon.WINDOW_FOR_RECORDER[j];
+        noisePoints[j] = noisePoints[j] * Common.WINDOW_FOR_RECORDER[j];
       }
-      for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
-        double freq = AudioCommon.FREQUENCIES_ORIGINAL[i];
+      for (int i = 0; i < Common.PIP_NUM; i++) {
+        double freq = Common.FREQUENCIES_ORIGINAL[i];
         Complex fourierCoeff = new Complex(0, 0);
         final Complex rotator = new Complex(0,
             -2.0 * Math.PI * freq / sampleRate).exp();
@@ -188,48 +168,48 @@
         noisePower[i][s] = fourierCoeff.multiply(fourierCoeff.conjugate()).abs();
       }
     }
-    for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+    for (int i = 0; i < Common.PIP_NUM; i++) {
       double meanNoisePower = 0;
-      for (int j = 0; j < AudioCommon.NOISE_SAMPLES; j++) {
+      for (int j = 0; j < Common.NOISE_SAMPLES; j++) {
         meanNoisePower += noisePower[i][j];
       }
-      meanNoisePower /= AudioCommon.NOISE_SAMPLES;
+      meanNoisePower /= Common.NOISE_SAMPLES;
       noiseDB[i] = 10 * Math.log10(meanNoisePower);
     }
 
     listener.sendMessage("Analyzing pips... Please wait...\n");
-    power = new double[AudioCommon.PIP_NUM][AudioCommon.REPETITIONS];
-    for (int i = 0; i < AudioCommon.PIP_NUM * AudioCommon.REPETITIONS; i++) {
-      if (i % AudioCommon.PIP_NUM == 0) {
-        listener.sendMessage("#" + (i / AudioCommon.PIP_NUM + 1) + "\n");
+    power = new double[Common.PIP_NUM][Common.REPETITIONS];
+    for (int i = 0; i < Common.PIP_NUM * Common.REPETITIONS; i++) {
+      if (i % Common.PIP_NUM == 0) {
+        listener.sendMessage("#" + (i / Common.PIP_NUM + 1) + "\n");
       }
 
       int pipExpectedStartI;
       pipExpectedStartI = prefixTotalLength
-          + Util.toLength(i * (AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S), sampleRate);
+          + Util.toLength(i * (Common.PIP_DURATION_S + Common.PAUSE_DURATION_S), sampleRate);
       // Cut out the data points for the current pip.
-      double[] pipPoints = new double[AudioCommon.WINDOW_FOR_RECORDER.length];
+      double[] pipPoints = new double[Common.WINDOW_FOR_RECORDER.length];
       System.arraycopy(data, pipExpectedStartI, pipPoints, 0, pipPoints.length);
-      for (int j = 0; j < AudioCommon.WINDOW_FOR_RECORDER.length; j++) {
-        pipPoints[j] = pipPoints[j] * AudioCommon.WINDOW_FOR_RECORDER[j];
+      for (int j = 0; j < Common.WINDOW_FOR_RECORDER.length; j++) {
+        pipPoints[j] = pipPoints[j] * Common.WINDOW_FOR_RECORDER[j];
       }
       Complex fourierCoeff = new Complex(0, 0);
       final Complex rotator = new Complex(0,
-          -2.0 * Math.PI * AudioCommon.FREQUENCIES[i] / sampleRate).exp();
+          -2.0 * Math.PI * Common.FREQUENCIES[i] / sampleRate).exp();
       Complex phasor = new Complex(1, 0);
       for (int j = 0; j < pipPoints.length; j++) {
         fourierCoeff = fourierCoeff.add(phasor.multiply(pipPoints[j]));
         phasor = phasor.multiply(rotator);
       }
       fourierCoeff = fourierCoeff.multiply(1.0 / pipPoints.length);
-      int j = AudioCommon.ORDER[i];
-      power[j % AudioCommon.PIP_NUM][j / AudioCommon.PIP_NUM] =
+      int j = Common.ORDER[i];
+      power[j % Common.PIP_NUM][j / Common.PIP_NUM] =
           fourierCoeff.multiply(fourierCoeff.conjugate()).abs();
     }
 
     // Calculate median of trials.
-    double[] dB = new double[AudioCommon.PIP_NUM];
-    for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
+    double[] dB = new double[Common.PIP_NUM];
+    for (int i = 0; i < Common.PIP_NUM; i++) {
       dB[i] = 10 * Math.log10(Util.median(power[i]));
     }
     return dB;
@@ -238,52 +218,41 @@
   /**
    * Align data using prefix. Package visible for unit testing.
    */
-  public int alignData() {
+  int alignData() {
     // Zeropadding samples to add in the correlation to avoid FFT wraparound.
-    final int zeroPad =
-            Util.toLength(AudioCommon.PREFIX_LENGTH_S, AudioCommon.RECORDING_SAMPLE_RATE_HZ) - 1;
-    int fftSize = Util.nextPowerOfTwo((int) Math.round(sampleRate * (AudioCommon.PREFIX_LENGTH_S
-              + AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S
-              + AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S + 0.5))
+    final int zeroPad = Util.toLength(Common.PREFIX_LENGTH_S, Common.RECORDING_SAMPLE_RATE_HZ) - 1;
+    int fftSize = Util.nextPowerOfTwo((int) Math.round(sampleRate * (Common.PREFIX_LENGTH_S
+            + Common.PAUSE_BEFORE_PREFIX_DURATION_S + Common.PAUSE_AFTER_PREFIX_DURATION_S + 0.5))
         + zeroPad);
 
     double[] dataCut = new double[fftSize - zeroPad];
     System.arraycopy(data, 0, dataCut, 0, fftSize - zeroPad);
     double[] xCorrDataPrefix = Util.computeCrossCorrelation(
         Util.padZeros(Util.toComplex(dataCut), fftSize),
-        Util.padZeros(Util.toComplex(AudioCommon.PREFIX_FOR_RECORDER), fftSize));
+        Util.padZeros(Util.toComplex(Common.PREFIX_FOR_RECORDER), fftSize));
     return Util.findMaxIndex(xCorrDataPrefix);
   }
 
-  public double[] getDB() {
+  double[] getDB() {
     return dB;
   }
 
-  public double[][] getPower() {
+  double[][] getPower() {
     return power;
   }
 
-  public double[] getNoiseDB() {
+  double[] getNoiseDB() {
     return noiseDB;
   }
 
-  public double getThreshold() {
+  double getThreshold() {
     return threshold;
   }
 
-  public boolean getResult() {
+  boolean getResult() {
     return result;
   }
 
-  public boolean isSilence() {
-    for (int i = 0; i < data.length; i++) {
-      if (Math.abs(data[i]) > SILENCE_THRESHOLD) {
-        return false;
-      }
-    }
-    return true;
-  }
-
   /**
    * An interface for listening a message publishing the progress of the analyzer.
    */
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundGenerator.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundGenerator.java
deleted file mode 100644
index 3ea5790..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/soundio/SoundGenerator.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.cts.verifier.audio.soundio;
-
-import com.android.cts.verifier.audio.audiolib.AudioCommon;
-import com.android.cts.verifier.audio.Util;
-
-/**
- * Sound generator.
- */
-public class SoundGenerator {
-
-  private static SoundGenerator instance;
-
-  private final byte[] generatedSound;
-  private final double[] sample;
-
-  private SoundGenerator() {
-    // Initialize sample.
-    int pipNum = AudioCommon.PIP_NUM;
-    int prefixTotalLength =
-          Util.toLength(AudioCommon.PREFIX_LENGTH_S, AudioCommon.PLAYING_SAMPLE_RATE_HZ)
-        + Util.toLength(AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S,
-                    AudioCommon.PLAYING_SAMPLE_RATE_HZ)
-        + Util.toLength(AudioCommon.PAUSE_AFTER_PREFIX_DURATION_S,
-                    AudioCommon.PLAYING_SAMPLE_RATE_HZ);
-    int repetitionLength = pipNum * Util.toLength(
-        AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S,
-            AudioCommon.PLAYING_SAMPLE_RATE_HZ);
-    int sampleLength = prefixTotalLength + AudioCommon.REPETITIONS * repetitionLength;
-    sample = new double[sampleLength];
-
-    // Fill sample with prefix.
-    System.arraycopy(AudioCommon.PREFIX_FOR_PLAYER, 0, sample,
-        Util.toLength(AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S,
-                AudioCommon.PLAYING_SAMPLE_RATE_HZ),
-        AudioCommon.PREFIX_FOR_PLAYER.length);
-
-    // Fill the sample.
-    for (int i = 0; i < pipNum * AudioCommon.REPETITIONS; i++) {
-      double[] pip = getPip(AudioCommon.WINDOW_FOR_PLAYER, AudioCommon.FREQUENCIES[i]);
-      System.arraycopy(pip, 0, sample,
-          prefixTotalLength + i * Util.toLength(
-              AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S,
-                  AudioCommon.PLAYING_SAMPLE_RATE_HZ),
-          pip.length);
-    }
-
-    // Convert sample to byte.
-    generatedSound = new byte[2 * sample.length];
-    int i = 0;
-    for (double dVal : sample) {
-      short val = (short) ((dVal * 32767));
-      generatedSound[i++] = (byte) (val & 0x00ff);
-      generatedSound[i++] = (byte) ((val & 0xff00) >>> 8);
-    }
-  }
-
-  public static SoundGenerator getInstance() {
-    if (instance == null) {
-      instance = new SoundGenerator();
-    }
-    return instance;
-  }
-
-  /**
-   * Gets a pip sample.
-   */
-  private static double[] getPip(double[] window, double frequency) {
-    int pipArrayLength = window.length;
-    double[] pipArray = new double[pipArrayLength];
-    double radPerSample = 2 * Math.PI / (AudioCommon.PLAYING_SAMPLE_RATE_HZ / frequency);
-    for (int i = 0; i < pipArrayLength; i++) {
-      pipArray[i] = window[i] * Math.sin(i * radPerSample);
-    }
-    return pipArray;
-  }
-
-  /**
-   * Get generated sound in byte[].
-   */
-  public byte[] getByte() {
-    return generatedSound;
-  }
-
-  /**
-   * Get sample in double[].
-   */
-  public double[] getSample() {
-    return sample;
-  }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/RmsHelper.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/RmsHelper.java
deleted file mode 100644
index 71d3ec1..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/wavelib/RmsHelper.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.cts.verifier.audio.wavelib;
-
-public class RmsHelper {
-    private double mRmsCurrent;
-    public int mBlockSize;
-    private int mShoutCount;
-    public boolean mRunning = false;
-
-    private short[] mAudioShortArray;
-
-    private DspBufferDouble mRmsSnapshots;
-    private int mShotIndex;
-
-    private final float mMinRmsDb;
-    private final float mMinRmsVal;
-
-    private static final float MAX_VAL = (float)(1 << 15);
-
-    public RmsHelper(int blockSize, int shotCount, float minRmsDb, float minRmsVal) {
-        mBlockSize = blockSize;
-        mShoutCount = shotCount;
-        mMinRmsDb = minRmsDb;
-        mMinRmsVal = minRmsVal;
-
-        reset();
-    }
-
-    public void reset() {
-        mAudioShortArray = new short[mBlockSize];
-        mRmsSnapshots = new DspBufferDouble(mShoutCount);
-        mShotIndex = 0;
-        mRmsCurrent = 0;
-        mRunning = false;
-    }
-
-    public void captureShot() {
-        if (mShotIndex >= 0 && mShotIndex < mRmsSnapshots.getSize()) {
-            mRmsSnapshots.setValue(mShotIndex++, mRmsCurrent);
-        }
-    }
-
-    public void setRunning(boolean running) {
-        mRunning = running;
-    }
-
-    public double getRmsCurrent() {
-        return mRmsCurrent;
-    }
-
-    public DspBufferDouble getRmsSnapshots() {
-        return mRmsSnapshots;
-    }
-
-    public boolean updateRms(PipeShort pipe, int channelCount, int channel) {
-        if (mRunning) {
-            int samplesAvailable = pipe.availableToRead();
-            while (samplesAvailable >= mBlockSize) {
-                pipe.read(mAudioShortArray, 0, mBlockSize);
-
-                double rmsTempSum = 0;
-                int count = 0;
-                for (int i = channel; i < mBlockSize; i += channelCount) {
-                    float value = mAudioShortArray[i] / MAX_VAL;
-
-                    rmsTempSum += value * value;
-                    count++;
-                }
-                float rms = count > 0 ? (float)Math.sqrt(rmsTempSum / count) : 0f;
-                if (rms < mMinRmsVal) {
-                    rms = mMinRmsVal;
-                }
-
-                double alpha = 0.9;
-                double total_rms = rms * alpha + mRmsCurrent * (1.0f - alpha);
-                mRmsCurrent = total_rms;
-
-                samplesAvailable = pipe.availableToRead();
-            }
-            return true;
-        }
-        return false;
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/CameraMuteToggleActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/CameraMuteToggleActivity.java
deleted file mode 100644
index 90fa098..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/camera/its/CameraMuteToggleActivity.java
+++ /dev/null
@@ -1,422 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.cts.verifier.camera.its;
-
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.ImageFormat;
-import android.graphics.SurfaceTexture;
-import android.hardware.camera2.CameraAccessException;
-import android.hardware.camera2.CameraCaptureSession;
-import android.hardware.camera2.CameraCharacteristics;
-import android.hardware.camera2.CameraDevice;
-import android.hardware.camera2.CameraManager;
-import android.hardware.camera2.CaptureRequest;
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
-import android.hardware.camera2.params.StreamConfigurationMap;
-import android.media.Image;
-import android.media.ImageReader;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.util.Log;
-import android.util.Size;
-import android.view.Surface;
-import android.view.TextureView;
-import android.widget.Button;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.compatibility.common.util.ResultType;
-import com.android.compatibility.common.util.ResultUnit;
-import com.android.cts.verifier.CtsVerifierReportLog;
-import com.android.cts.verifier.PassFailButtons;
-import com.android.cts.verifier.R;
-import com.android.ex.camera2.blocking.BlockingCameraManager;
-import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
-import com.android.ex.camera2.blocking.BlockingSessionCallback;
-import com.android.ex.camera2.blocking.BlockingStateCallback;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * Test for manual verification of camera privacy hardware switches
- * This test verifies that devices which implement camera hardware
- * privacy toggles enforce sensor privacy when toggles are enabled.
- * - The video stream should be muted:
- * - camera preview & capture should be blank
- * - A dialog or notification should be shown that informs
- * the user that the sensor privacy is enabled.
- */
-public class CameraMuteToggleActivity extends PassFailButtons.Activity
-        implements TextureView.SurfaceTextureListener,
-        ImageReader.OnImageAvailableListener {
-
-    private static final String TAG = "CameraMuteToggleActivity";
-    private static final int SESSION_READY_TIMEOUT_MS = 5000;
-    private static final int DEFAULT_CAMERA_IDX = 0;
-
-    private TextureView mPreviewView;
-    private SurfaceTexture mPreviewTexture;
-    private Surface mPreviewSurface;
-
-    private ImageView mImageView;
-
-    private CameraManager mCameraManager;
-    private HandlerThread mCameraThread;
-    private Handler mCameraHandler;
-    private BlockingCameraManager mBlockingCameraManager;
-    private CameraCharacteristics mCameraCharacteristics;
-    private BlockingStateCallback mCameraListener;
-
-    private BlockingSessionCallback mSessionListener;
-    private CaptureRequest.Builder mPreviewRequestBuilder;
-    private CaptureRequest mPreviewRequest;
-    private CaptureRequest.Builder mStillCaptureRequestBuilder;
-    private CaptureRequest mStillCaptureRequest;
-
-    private CameraCaptureSession mCaptureSession;
-    private CameraDevice mCameraDevice;
-
-    SizeComparator mSizeComparator = new SizeComparator();
-
-    private Size mPreviewSize;
-    private Size mJpegSize;
-    private ImageReader mJpegImageReader;
-
-    private CameraCaptureSession.CaptureCallback mCaptureCallback =
-            new CameraCaptureSession.CaptureCallback() {
-            };
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.cam_hw_toggle);
-
-        setPassFailButtonClickListeners();
-
-        mPreviewView = findViewById(R.id.preview_view);
-        mImageView = findViewById(R.id.image_view);
-
-        mPreviewView.setSurfaceTextureListener(this);
-
-        mCameraManager = getSystemService(CameraManager.class);
-
-        setInfoResources(R.string.camera_hw_toggle_test, R.string.camera_hw_toggle_test_info, -1);
-
-        // Enable Pass button only after taking photo
-        setPassButtonEnabled(false);
-        setTakePictureButtonEnabled(false);
-
-        mBlockingCameraManager = new BlockingCameraManager(mCameraManager);
-        mCameraListener = new BlockingStateCallback();
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-
-        startBackgroundThread();
-
-        Exception cameraSetupException = null;
-        boolean enablePassButton = false;
-        try {
-            final String[] camerasList = mCameraManager.getCameraIdList();
-            if (camerasList.length > 0) {
-                String cameraId = mCameraManager.getCameraIdList()[DEFAULT_CAMERA_IDX];
-                setUpCamera(cameraId);
-            } else {
-                showCameraErrorText("");
-            }
-        } catch (CameraAccessException e) {
-            cameraSetupException = e;
-            // Enable Pass button for cameras that do not support mute patterns
-            // and will disconnect clients if sensor privacy is enabled
-            enablePassButton = (e.getReason() == CameraAccessException.CAMERA_DISABLED);
-        } catch (BlockingOpenException e) {
-            cameraSetupException = e;
-            enablePassButton = e.wasDisconnected();
-        } finally {
-            if (cameraSetupException != null) {
-                cameraSetupException.printStackTrace();
-                showCameraErrorText(cameraSetupException.getMessage());
-                setPassButtonEnabled(enablePassButton);
-            }
-        }
-    }
-
-    private void showCameraErrorText(String errorMsg) {
-        TextView instructionsText = findViewById(R.id.instruction_text);
-        instructionsText.setText(R.string.camera_hw_toggle_test_no_camera);
-        instructionsText.append(errorMsg);
-        setTakePictureButtonEnabled(false);
-    }
-
-    @Override
-    public void onPause() {
-        shutdownCamera();
-        stopBackgroundThread();
-
-        super.onPause();
-    }
-
-    @Override
-    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
-            int width, int height) {
-        mPreviewTexture = surfaceTexture;
-
-        mPreviewSurface = new Surface(mPreviewTexture);
-
-        if (mCameraDevice != null) {
-            startPreview();
-        }
-    }
-
-    @Override
-    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
-        // Ignored, Camera does all the work for us
-        Log.v(TAG, "onSurfaceTextureSizeChanged: " + width + " x " + height);
-    }
-
-    @Override
-    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
-        mPreviewTexture = null;
-        return true;
-    }
-
-    @Override
-    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
-        // Invoked every time there's a new Camera preview frame
-    }
-
-    @Override
-    public void onImageAvailable(ImageReader reader) {
-        Image img = null;
-        try {
-            img = reader.acquireNextImage();
-            if (img == null) {
-                Log.d(TAG, "Invalid image!");
-                return;
-            }
-            final int format = img.getFormat();
-
-            Bitmap imgBitmap = null;
-            if (format == ImageFormat.JPEG) {
-                ByteBuffer jpegBuffer = img.getPlanes()[0].getBuffer();
-                jpegBuffer.rewind();
-                byte[] jpegData = new byte[jpegBuffer.limit()];
-                jpegBuffer.get(jpegData);
-                imgBitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
-                img.close();
-            } else {
-                Log.i(TAG, "Unsupported image format: " + format);
-            }
-            if (imgBitmap != null) {
-                final Bitmap bitmap = imgBitmap;
-                final boolean isMuted = isBitmapMuted(imgBitmap);
-                runOnUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        mImageView.setImageBitmap(bitmap);
-                        // enable pass button if image is muted (black)
-                        setPassButtonEnabled(isMuted);
-                    }
-                });
-            }
-        } catch (java.lang.IllegalStateException e) {
-            // Swallow exceptions
-            e.printStackTrace();
-        } finally {
-            if (img != null) {
-                img.close();
-            }
-        }
-    }
-
-    private boolean isBitmapMuted(final Bitmap imgBitmap) {
-        // black images may have pixels with values > 0
-        // because of JPEG compression artifacts
-        final float COLOR_THRESHOLD = 0.02f;
-        for (int y = 0; y < imgBitmap.getHeight(); y++) {
-            for (int x = 0; x < imgBitmap.getWidth(); x++) {
-                Color pixelColor = Color.valueOf(imgBitmap.getPixel(x, y));
-                if (pixelColor.red() > COLOR_THRESHOLD || pixelColor.green() > COLOR_THRESHOLD
-                        || pixelColor.blue() > COLOR_THRESHOLD) {
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
-    private class SizeComparator implements Comparator<Size> {
-        @Override
-        public int compare(Size lhs, Size rhs) {
-            long lha = lhs.getWidth() * lhs.getHeight();
-            long rha = rhs.getWidth() * rhs.getHeight();
-            if (lha == rha) {
-                lha = lhs.getWidth();
-                rha = rhs.getWidth();
-            }
-            return (lha < rha) ? -1 : (lha > rha ? 1 : 0);
-        }
-    }
-
-    private void setUpCamera(String cameraId) throws CameraAccessException, BlockingOpenException {
-        shutdownCamera();
-
-        mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId);
-        mCameraDevice = mBlockingCameraManager.openCamera(cameraId,
-                mCameraListener, mCameraHandler);
-
-        StreamConfigurationMap config =
-                mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
-        Size[] jpegSizes = config.getOutputSizes(ImageFormat.JPEG);
-        Arrays.sort(jpegSizes, mSizeComparator);
-        // choose smallest image size, image capture is not the point of this test
-        mJpegSize = jpegSizes[0];
-
-        mJpegImageReader = ImageReader.newInstance(
-                mJpegSize.getWidth(), mJpegSize.getHeight(), ImageFormat.JPEG, 1);
-        mJpegImageReader.setOnImageAvailableListener(this, mCameraHandler);
-
-        if (mPreviewTexture != null) {
-            startPreview();
-        }
-    }
-
-    private void shutdownCamera() {
-        if (null != mCaptureSession) {
-            mCaptureSession.close();
-            mCaptureSession = null;
-        }
-        if (null != mCameraDevice) {
-            mCameraDevice.close();
-            mCameraDevice = null;
-        }
-        if (null != mJpegImageReader) {
-            mJpegImageReader.close();
-            mJpegImageReader = null;
-        }
-    }
-
-    /**
-     * Starts a background thread and its {@link Handler}.
-     */
-    private void startBackgroundThread() {
-        mCameraThread = new HandlerThread("CameraThreadBackground");
-        mCameraThread.start();
-        mCameraHandler = new Handler(mCameraThread.getLooper());
-    }
-
-    /**
-     * Stops the background thread and its {@link Handler}.
-     */
-    private void stopBackgroundThread() {
-        mCameraThread.quitSafely();
-        try {
-            mCameraThread.join();
-            mCameraThread = null;
-            mCameraHandler = null;
-        } catch (InterruptedException e) {
-            e.printStackTrace();
-        }
-    }
-
-    private Size getPreviewSize(int minWidth) {
-        StreamConfigurationMap config =
-                mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
-        Size[] outputSizes = config.getOutputSizes(SurfaceTexture.class);
-        Arrays.sort(outputSizes, mSizeComparator);
-        // choose smallest image size that's at least minWidth
-        // image capture is not the point of this test
-        for (Size outputSize : outputSizes) {
-            if (outputSize.getWidth() > minWidth) {
-                return outputSize;
-            }
-        }
-        return outputSizes[0];
-    }
-
-    private void startPreview() {
-        try {
-            mPreviewSize = getPreviewSize(256);
-
-            mPreviewTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
-            mPreviewRequestBuilder =
-                    mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
-            mPreviewRequestBuilder.addTarget(mPreviewSurface);
-
-            mStillCaptureRequestBuilder =
-                    mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
-            mStillCaptureRequestBuilder.addTarget(mPreviewSurface);
-            mStillCaptureRequestBuilder.addTarget(mJpegImageReader.getSurface());
-
-            mSessionListener = new BlockingSessionCallback();
-            List<Surface> outputSurfaces = new ArrayList<Surface>(/*capacity*/3);
-            outputSurfaces.add(mPreviewSurface);
-            outputSurfaces.add(mJpegImageReader.getSurface());
-            mCameraDevice.createCaptureSession(outputSurfaces, mSessionListener, mCameraHandler);
-            mCaptureSession = mSessionListener.waitAndGetSession(/*timeoutMs*/3000);
-
-            mPreviewRequest = mPreviewRequestBuilder.build();
-            mStillCaptureRequest = mStillCaptureRequestBuilder.build();
-
-            mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mCameraHandler);
-
-            setTakePictureButtonEnabled(true);
-        } catch (CameraAccessException e) {
-            e.printStackTrace();
-        }
-    }
-
-    private void takePicture() {
-        try {
-            mCaptureSession.stopRepeating();
-            mSessionListener.getStateWaiter().waitForState(
-                    BlockingSessionCallback.SESSION_READY, SESSION_READY_TIMEOUT_MS);
-
-            mCaptureSession.capture(mStillCaptureRequest, mCaptureCallback, mCameraHandler);
-        } catch (CameraAccessException e) {
-            e.printStackTrace();
-        }
-    }
-
-    private void setPassButtonEnabled(boolean enabled) {
-        ImageButton pass_button = findViewById(R.id.pass_button);
-        pass_button.setEnabled(enabled);
-    }
-
-    private void setTakePictureButtonEnabled(boolean enabled) {
-        Button takePhoto = findViewById(R.id.take_picture_button);
-        takePhoto.setOnClickListener(v -> takePicture());
-        takePhoto.setEnabled(enabled);
-    }
-
-    @Override
-    public void recordTestResults() {
-        CtsVerifierReportLog reportLog = getReportLog();
-        reportLog.submit();
-    }
-}
diff --git a/common/device-side/bedstead/testapp/manifests/RemoteDPCManifest.xml b/common/device-side/bedstead/testapp/manifests/RemoteDPCManifest.xml
index f107562..8682ef6 100644
--- a/common/device-side/bedstead/testapp/manifests/RemoteDPCManifest.xml
+++ b/common/device-side/bedstead/testapp/manifests/RemoteDPCManifest.xml
@@ -34,6 +34,16 @@
                 <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
             </intent-filter>
         </receiver>
+
+        <!-- This activity is used to show "Work Policy Info" in Settings for Managed Devices -->
+        <activity android:name=".WorkPolicyInfoActivity"
+                  android:exported="true"
+                  android:launchMode="singleTask">
+            <intent-filter>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <action android:name="android.settings.SHOW_WORK_POLICY_INFO"/>
+            </intent-filter>
+        </activity>
     </application>
     <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="28"/>
 </manifest>
\ No newline at end of file
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java
index b2d496c..a022936 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java
@@ -31,36 +31,46 @@
     private static final int FIRST_API_LEVEL =
             SystemProperties.getInt("ro.product.first_api_level", 0);
 
+    private static void verifyVersion(int version) {
+        if (version == Build.VERSION_CODES.CUR_DEVELOPMENT) {
+            throw new RuntimeException("Invalid version: " + version);
+        }
+    }
+
     public static boolean isBefore(int version) {
+        verifyVersion(version);
         return Build.VERSION.SDK_INT < version;
     }
 
     public static boolean isBefore(String version) {
-        return Build.VERSION.SDK_INT < resolveVersionString(version);
+        return isBefore(resolveVersionString(version));
     }
 
     public static boolean isAfter(int version) {
+        verifyVersion(version);
         return Build.VERSION.SDK_INT > version;
     }
 
     public static boolean isAfter(String version) {
-        return Build.VERSION.SDK_INT > resolveVersionString(version);
+        return isAfter(resolveVersionString(version));
     }
 
     public static boolean isAtLeast(int version) {
+        verifyVersion(version);
         return Build.VERSION.SDK_INT >= version;
     }
 
     public static boolean isAtLeast(String version) {
-        return Build.VERSION.SDK_INT >= resolveVersionString(version);
+        return isAtLeast(resolveVersionString(version));
     }
 
     public static boolean isAtMost(int version) {
+        verifyVersion(version);
         return Build.VERSION.SDK_INT <= version;
     }
 
     public static boolean isAtMost(String version) {
-        return Build.VERSION.SDK_INT <= resolveVersionString(version);
+        return isAtMost(resolveVersionString(version));
     }
 
     public static int getApiLevel() {
@@ -68,19 +78,21 @@
     }
 
     public static boolean isFirstApiBefore(int version) {
+        verifyVersion(version);
         return FIRST_API_LEVEL < version;
     }
 
     public static boolean isFirstApiBefore(String version) {
-        return FIRST_API_LEVEL < resolveVersionString(version);
+        return isFirstApiBefore(resolveVersionString(version));
     }
 
     public static boolean isFirstApiAfter(int version) {
+        verifyVersion(version);
         return FIRST_API_LEVEL > version;
     }
 
     public static boolean isFirstApiAfter(String version) {
-        return FIRST_API_LEVEL > resolveVersionString(version);
+        return isFirstApiAfter(resolveVersionString(version));
     }
 
     public static boolean isFirstApiAtLeast(int version) {
@@ -88,7 +100,7 @@
     }
 
     public static boolean isFirstApiAtLeast(String version) {
-        return FIRST_API_LEVEL >= resolveVersionString(version);
+        return isFirstApiAtLeast(resolveVersionString(version));
     }
 
     public static boolean isFirstApiAtMost(int version) {
@@ -96,7 +108,7 @@
     }
 
     public static boolean isFirstApiAtMost(String version) {
-        return FIRST_API_LEVEL <= resolveVersionString(version);
+        return isFirstApiAtMost(resolveVersionString(version));
     }
 
     public static int getFirstApiLevel() {
diff --git a/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java b/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java
index 1993f30..a176499 100644
--- a/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java
+++ b/hostsidetests/appcompat/strictjavapackages/src/android/compat/sjp/cts/StrictJavaPackagesTest.java
@@ -30,12 +30,14 @@
 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
 import com.android.modules.utils.build.testing.DeviceSdkLevel;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.INativeDevice;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.TestInformation;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
 import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
+import com.android.tradefed.util.FileUtil;
 
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableCollection;
@@ -52,10 +54,13 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -79,6 +84,7 @@
     private static ImmutableList<String> sSharedLibJars;
     private static ImmutableList<SharedLibraryInfo> sSharedLibs;
     private static ImmutableMultimap<String, String> sJarsToClasses;
+    private static ImmutableMultimap<String, String> sJarsToFiles;
 
     private DeviceSdkLevel mDeviceSdkLevel;
 
@@ -780,6 +786,8 @@
                 .filter(file -> !file.contains("com.google.android.gms"))
                 .collect(ImmutableList.toImmutableList());
 
+        final ImmutableSetMultimap.Builder<String, String> jarsToFiles =
+                ImmutableSetMultimap.builder();
         final ImmutableSetMultimap.Builder<String, String> jarsToClasses =
                 ImmutableSetMultimap.builder();
         Stream.of(sBootclasspathJars.stream(),
@@ -787,21 +795,32 @@
                         sSharedLibJars.stream())
                 .reduce(Stream::concat).orElseGet(Stream::empty)
                 .parallel()
-                .forEach(jar -> {
+                .forEach(jarPath -> {
+                    File jar = null;
                     try {
+                        jar = pullJarFromDevice(testInfo.getDevice(), jarPath);
+
+                        ImmutableSet<String> files = getJarFileContents(jar);
+                        synchronized (jarsToFiles) {
+                            jarsToFiles.putAll(jarPath, files);
+                        }
+
                         ImmutableSet<String> classes =
-                                Classpaths.getClassDefsFromJar(testInfo.getDevice(), jar).stream()
+                                Classpaths.getClassDefsFromJar(jar).stream()
                                         .map(ClassDef::getType)
                                         // Inner classes always go with their parent.
                                         .filter(className -> !className.contains("$"))
                                         .collect(ImmutableSet.toImmutableSet());
                         synchronized (jarsToClasses) {
-                            jarsToClasses.putAll(jar, classes);
+                            jarsToClasses.putAll(jarPath, classes);
                         }
                     } catch (DeviceNotAvailableException | IOException e) {
                         throw new RuntimeException(e);
+                    } finally {
+                        FileUtil.deleteFile(jar);
                     }
                 });
+        sJarsToFiles = jarsToFiles.build();
         sJarsToClasses = jarsToClasses.build();
     }
 
@@ -965,9 +984,11 @@
         Arrays.stream(collectApkInApexPaths())
                 .parallel()
                 .forEach(apk -> {
+                    File apkFile = null;
                     try {
+                        apkFile = pullJarFromDevice(getDevice(), apk);
                         final ImmutableSet<String> apkClasses =
-                                Classpaths.getClassDefsFromJar(getDevice(), apk).stream()
+                                Classpaths.getClassDefsFromJar(apkFile).stream()
                                         .map(ClassDef::getType)
                                         .collect(ImmutableSet.toImmutableSet());
                         // b/226559955: The directory paths containing APKs contain the build ID,
@@ -991,6 +1012,8 @@
                         }
                     } catch (Exception e) {
                         throw new RuntimeException(e);
+                    } finally {
+                        FileUtil.deleteFile(apkFile);
                     }
                 });
         assertThat(perApkClasspathDuplicates).isEmpty();
@@ -1027,6 +1050,46 @@
                 ).isEmpty();
     }
 
+    /**
+     * Ensure that there are no kotlin files in BOOTCLASSPATH, SYSTEMSERVERCLASSPATH
+     * and shared library jars.
+     */
+    @Test
+    public void testNoKotlinFilesInClasspaths() {
+        ImmutableList<String> kotlinFiles =
+                Stream.of(sBootclasspathJars.stream(),
+                        sSystemserverclasspathJars.stream(),
+                        sSharedLibJars.stream())
+                .reduce(Stream::concat).orElseGet(Stream::empty)
+                .parallel()
+                .filter(jarPath -> {
+                    return sJarsToFiles
+                            .get(jarPath)
+                            .stream()
+                            .anyMatch(file -> file.contains(".kotlin_builtins")
+                                    || file.contains(".kotlin_module"));
+                })
+                .collect(ImmutableList.toImmutableList());
+        assertThat(kotlinFiles).isEmpty();
+    }
+
+    private static File pullJarFromDevice(INativeDevice device,
+            String remoteJarPath) throws DeviceNotAvailableException {
+        File jar = device.pullFile(remoteJarPath);
+        if (jar == null) {
+            throw new IllegalStateException("could not pull remote file " + remoteJarPath);
+        }
+        return jar;
+    }
+
+    private static ImmutableSet<String> getJarFileContents(File jar) throws IOException {
+        try (JarFile jarFile = new JarFile(jar)) {
+            return jarFile.stream()
+                    .map(JarEntry::getName)
+                    .collect(ImmutableSet.toImmutableSet());
+        }
+    }
+
     private boolean isLegacyAndroidxDependency(
             ImmutableMap<String, ImmutableSet<String>> legacyExemptAndroidxSharedLibsJarToClasses,
             String jar, String className) {
diff --git a/hostsidetests/appsecurity/OWNERS b/hostsidetests/appsecurity/OWNERS
index d027e21..37c15ac 100644
--- a/hostsidetests/appsecurity/OWNERS
+++ b/hostsidetests/appsecurity/OWNERS
@@ -14,6 +14,17 @@
 # Bug component: 36137 = per-file SharedUserIdTest.java
 # Bug component: 36137 = per-file SplitTests.java
 
+# Storage bug component
+# Bug component: 46626 = per-file *Adoptable*
+# Bug component: 46626 = per-file *DirectBoot*
+# Bug component: 46626 = per-file *Storage*
+# Bug component: 46626 = per-file *Documents*
+# Bug component: 46626 = per-file ScopedDirectoryAccessTest.java
+
+# DocsUI bug component
+# Bug component: 46626 = per-file *Documents*
+# Bug component: 46626 = per-file ScopedDirectoryAccessTest.java
+
 patb@google.com
 per-file AccessSerialNumberTest.java = ewol@google.com
 per-file ApexSignatureVerificationTest.java = dariofreni@google.com
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
index 6a65c5b..099a0ab 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
@@ -1131,6 +1131,11 @@
     }
 
     @Test
+    // There is a minor bug which, alghough fixed in sc-dev (aosp/1834457),
+    // cannot be propagated to the already released sc-release branche
+    // (b/234145920), where mainline-modules are tested.
+    // Skip this test in S to avoid failures in outdated targets.
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testAppendUpdatesMtime() throws Exception {
         writeAndCheckMtime(true);
     }
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/AppOpsTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/AppOpsTests.java
index f07f9d3..c7feda6 100644
--- a/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/AppOpsTests.java
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/appops/AppOpsTests.java
@@ -61,13 +61,12 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        // Temporarily commented out until the Trusted Hotword requirement is enforced again.
-        // mTransformedFromOp.clear();
-        // // The hotword op is allowed to all UIDs on TV and Auto devices.
-        // if (!(DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)
-        //         || DeviceUtils.hasFeature(getDevice(), FEATURE_LEANBACK_ONLY))) {
-        //     mTransformedFromOp.put(APP_OP_RECORD_AUDIO, APP_OP_RECORD_AUDIO_HOTWORD);
-        // }
+        mTransformedFromOp.clear();
+        // The hotword op is allowed to all UIDs on TV and Auto devices.
+        if (!(DeviceUtils.hasFeature(getDevice(), FEATURE_AUTOMOTIVE)
+                || DeviceUtils.hasFeature(getDevice(), FEATURE_LEANBACK_ONLY))) {
+            mTransformedFromOp.put(APP_OP_RECORD_AUDIO, APP_OP_RECORD_AUDIO_HOTWORD);
+        }
 
         assertThat(mCtsBuild).isNotNull();
         ConfigUtils.removeConfig(getDevice());
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
index cc8022d3..28051e2 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
@@ -37,6 +37,9 @@
 import android.app.Activity;
 import android.content.ClipData;
 import android.content.Intent;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
 import android.net.Uri;
 import android.provider.MediaStore;
 
@@ -384,13 +387,22 @@
         // Test 2: Click Mute Button
         // Click to unmute the audio
         clickAndWait(muteButton);
+
+        waitForBinderCallsToComplete();
+
         // Check that mute button state is unmute, i.e., it shows `volume up` icon
         assertMuteButtonState(muteButton, /* isMuted */ false);
         // Click on the muteButton and check that mute button status is now 'mute'
         clickAndWait(muteButton);
+
+        waitForBinderCallsToComplete();
+
         assertMuteButtonState(muteButton, /* isMuted */ true);
         // Click on the muteButton and check that mute button status is now unmute
         clickAndWait(muteButton);
+
+        waitForBinderCallsToComplete();
+
         assertMuteButtonState(muteButton, /* isMuted */ false);
 
         // Test 3: Next preview resumes mute state
@@ -398,6 +410,8 @@
         mDevice.pressBack();
         clickAndWait(findViewSelectedButton());
 
+        waitForBinderCallsToComplete();
+
         // check that player controls are visible
         assertPlayerControlsVisible(playPauseButton, muteButton);
         assertMuteButtonState(muteButton, /* isMuted */ false);
@@ -421,6 +435,9 @@
         assertMuteButtonState(muteButton, /* isMuted */ true);
         // Swipe to next page and check that muteButton is in mute state.
         swipeLeftAndWait();
+
+        waitForBinderCallsToComplete();
+
         // set-up and wait for player controls to be sticky
         setUpAndAssertStickyPlayerControls(playerView, playPauseButton, muteButton);
         assertMuteButtonState(muteButton, /* isMuted */ true);
@@ -428,9 +445,15 @@
         // Test 2: Swipe resumes mute state, with state of mute button 'volume up' / 'unmute'
         // Click muteButton again to check the next video resumes the previous video's mute state
         clickAndWait(muteButton);
+
+        waitForBinderCallsToComplete();
+
         assertMuteButtonState(muteButton, /* isMuted */ false);
         // check that next video resumed previous video's mute state
         swipeLeftAndWait();
+
+        waitForBinderCallsToComplete();
+
         // Wait for 1s before checking Play/Pause button's visibility
         playPauseButton.waitForExists(1000);
         // check that player controls are visible
@@ -442,6 +465,78 @@
     }
 
     @Test
+    public void testVideoPreviewAudioFocus() throws Exception {
+        final int[] focusStateForTest = new int[1];
+        final AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+        AudioFocusRequest audioFocusRequest =
+                new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+                .setAudioAttributes(new AudioAttributes.Builder()
+                        .setContentType(AudioAttributes.USAGE_MEDIA)
+                        .setUsage(AudioAttributes.CONTENT_TYPE_MOVIE)
+                        .build())
+                .setWillPauseWhenDucked(true)
+                .setAcceptsDelayedFocusGain(false)
+                .setOnAudioFocusChangeListener(focusChange -> {
+                    if (focusChange == AudioManager.AUDIOFOCUS_LOSS
+                            || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
+                            || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+                        focusStateForTest[0] = focusChange;
+                    }
+                })
+                .build();
+
+        // Request AudioFocus
+        assertWithMessage("Expected requestAudioFocus result")
+                .that(audioManager.requestAudioFocus(audioFocusRequest))
+                .isEqualTo(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+
+        // Launch Preview
+        launchPreviewMultipleWithVideos(/* videoCount */ 2);
+        // Video preview launches in mute mode, hence, test's audio focus shouldn't be lost when
+        // video preview starts
+        assertThat(focusStateForTest[0]).isEqualTo(0);
+
+        final UiObject muteButton = findMuteButton();
+        // unmute the audio of video preview
+        clickAndWait(muteButton);
+
+        // Remote video preview involves binder calls
+        // Wait for Binder calls to complete and device to be idle
+        MediaStore.waitForIdle(mContext.getContentResolver());
+        mDevice.waitForIdle();
+
+        assertMuteButtonState(muteButton, /* isMuted */ false);
+
+        // Verify that test lost the audio focus because PhotoPicker has requested audio focus now.
+        assertThat(focusStateForTest[0]).isEqualTo(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+
+        // Reset the focusStateForTest to verify test loses audio focus when video preview is
+        // launched with unmute state
+        focusStateForTest[0] = 0;
+        // Abandon the audio focus before requesting again. This is necessary to reduce test flakes
+        audioManager.abandonAudioFocusRequest(audioFocusRequest);
+        // Request AudioFocus from test again
+        assertWithMessage("Expected requestAudioFocus result")
+                .that(audioManager.requestAudioFocus(audioFocusRequest))
+                        .isEqualTo(AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+
+        // Wait for PhotoPicker to lose Audio Focus
+        findPlayButton().waitForExists(SHORT_TIMEOUT);
+        // Test requesting audio focus will make PhotoPicker lose audio focus, Verify video is
+        // paused when PhotoPicker loses audio focus.
+        assertWithMessage("PlayPause button's content description")
+                .that(findPlayPauseButton().getContentDescription())
+                .isEqualTo("Play");
+
+        // Swipe to next video and verify preview gains audio focus
+        swipeLeftAndWait();
+        findPauseButton().waitForExists(SHORT_TIMEOUT);
+        // Video preview is now in unmute mode. Hence, PhotoPicker will request audio focus. Verify
+        // that test lost the audio focus.
+        assertThat(focusStateForTest[0]).isEqualTo(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
+    }
+
+    @Test
     @Ignore("Re-enable once we find work around for b/226318844")
     public void testMultiSelect_previewVideoControlsVisibility() throws Exception {
         launchPreviewMultipleWithVideos(/* videoCount */ 3);
@@ -576,6 +671,10 @@
         final long playbackStartTimeout = 10000;
         (findPreviewVideoImageView()).waitUntilGone(playbackStartTimeout);
 
+        waitForBinderCallsToComplete();
+    }
+
+    private void waitForBinderCallsToComplete() {
         // Wait for Binder calls to complete and device to be idle
         MediaStore.waitForIdle(mContext.getContentResolver());
         mDevice.waitForIdle();
@@ -650,6 +749,14 @@
                 REGEX_PACKAGE_NAME + ":id/exo_play_pause"));
     }
 
+    private static UiObject findPauseButton() {
+        return new UiObject(new UiSelector().descriptionContains("Pause"));
+    }
+
+    private static UiObject findPlayButton() {
+        return new UiObject(new UiSelector().descriptionContains("Play"));
+    }
+
     private static UiObject findPreviewVideoImageView() {
         return new UiObject(new UiSelector().resourceIdMatches(
                 REGEX_PACKAGE_NAME + ":id/preview_video_image"));
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationSynchronicityTests.java b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationSynchronicityTests.java
index 51033bb..bb83db1 100644
--- a/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationSynchronicityTests.java
+++ b/tests/framework/base/windowmanager/src/android/server/wm/WindowInsetsAnimationSynchronicityTests.java
@@ -64,7 +64,6 @@
 import com.android.compatibility.common.util.SystemUtil;
 import com.android.compatibility.common.util.WindowUtil;
 
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 
@@ -84,13 +83,11 @@
 
     private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
 
-    @Ignore("b/168446060")
     @Test
     public void testShowAndHide_renderSynchronouslyBetweenImeWindowAndAppContent() throws Throwable {
         runTest(false /* useControlApi */);
     }
 
-    @Ignore("b/168446060")
     @Test
     public void testControl_rendersSynchronouslyBetweenImeWindowAndAppContent() throws Throwable {
         runTest(true /* useControlApi */);
diff --git a/tests/ondevicepersonalization/TEST_MAPPING b/tests/ondevicepersonalization/TEST_MAPPING
new file mode 100644
index 0000000..8d96b25
--- /dev/null
+++ b/tests/ondevicepersonalization/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+        "name": "CtsOnDevicePersonalizationTestCases"
+    }
+  ]
+}
diff --git a/tests/ondevicepersonalization/src/android/ondevicepersonalization/cts/OnDevicePersonalizationServiceTest.java b/tests/ondevicepersonalization/src/android/ondevicepersonalization/cts/OnDevicePersonalizationServiceTest.java
index 16db9a9..234a0cd 100644
--- a/tests/ondevicepersonalization/src/android/ondevicepersonalization/cts/OnDevicePersonalizationServiceTest.java
+++ b/tests/ondevicepersonalization/src/android/ondevicepersonalization/cts/OnDevicePersonalizationServiceTest.java
@@ -19,7 +19,7 @@
 import static org.junit.Assert.assertEquals;
 
 import android.content.Context;
-import android.ondevicepersonalization.OnDevicePersonalizationManager;
+import android.ondevicepersonalization.OnDevicePersonalizationManaging;
 
 import androidx.test.core.app.ApplicationProvider;
 
@@ -29,17 +29,17 @@
 import org.junit.runners.JUnit4;
 
 /**
- * Test of {@link OnDevicePersonalizationManager}
+ * Test of {@link OnDevicePersonalizationManaging}
  */
 @RunWith(JUnit4.class)
 public class OnDevicePersonalizationServiceTest {
     private Context mContext;
-    private OnDevicePersonalizationManager mService;
+    private OnDevicePersonalizationManaging mService;
 
     @Before
     public void setup() throws Exception {
         mContext = ApplicationProvider.getApplicationContext();
-        mService = new OnDevicePersonalizationManager(mContext);
+        mService = mContext.getSystemService(OnDevicePersonalizationManaging.class);
     }
 
     @Test
diff --git a/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt b/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
index cdbd58c..4b91ee0 100644
--- a/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
+++ b/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
@@ -132,6 +132,14 @@
     }
 }
 
+fun runPermissionEventCleanupJob(context: Context) {
+    eventually {
+        runShellCommandOrThrow("cmd jobscheduler run -u " +
+            "${Process.myUserHandle().identifier} -f " +
+            "${context.packageManager.permissionControllerPackageName} 3")
+    }
+}
+
 inline fun withApp(
     apk: String,
     packageName: String,
@@ -211,6 +219,7 @@
 
 fun goHome() {
     runShellCommandOrThrow("input keyevent KEYCODE_HOME")
+    waitForIdle()
 }
 
 /**
diff --git a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt b/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
index 7de2727..b724be8 100644
--- a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
+++ b/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
@@ -29,6 +29,7 @@
 import android.net.Uri
 import android.os.Build
 import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
 import android.support.test.uiautomator.By
 import android.support.test.uiautomator.BySelector
 import android.support.test.uiautomator.UiObject2
@@ -38,6 +39,7 @@
 import androidx.test.InstrumentationRegistry
 import androidx.test.filters.SdkSuppress
 import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.DeviceConfigStateChangerRule
 import com.android.compatibility.common.util.DisableAnimationRule
 import com.android.compatibility.common.util.FreezeRotationRule
 import com.android.compatibility.common.util.MatcherUtils.hasTextThat
@@ -82,7 +84,6 @@
  */
 @RunWith(AndroidJUnit4::class)
 class AutoRevokeTest {
-
     private val context: Context = InstrumentationRegistry.getTargetContext()
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
 
@@ -94,8 +95,14 @@
     private lateinit var preMinVersionApkPath: String
     private lateinit var preMinVersionAppPackageName: String
 
+    @Rule
+    @JvmField
+    val storeExactTimeRule = DeviceConfigStateChangerRule(context,
+        DeviceConfig.NAMESPACE_PERMISSIONS, STORE_EXACT_TIME_KEY, "true")
+
     companion object {
         const val LOG_TAG = "AutoRevokeTest"
+        private const val STORE_EXACT_TIME_KEY = "permission_changes_store_exact_time"
 
         @JvmStatic
         @BeforeClass
@@ -253,6 +260,61 @@
 
     @AppModeFull(reason = "Uses separate apps for testing")
     @Test
+    fun testAppWithPermissionsChangedRecently_doesNotGetPermissionRevoked() {
+        val unusedThreshold = 15_000L
+        withUnusedThresholdMs(unusedThreshold) {
+            withDummyApp {
+                // Setup
+                // Ensure app is considered unused and then change permission
+                Thread.sleep(unusedThreshold)
+                goToPermissions()
+                click("Calendar")
+                click("Allow")
+                goBack()
+                goBack()
+                goBack()
+
+                // Run
+                runAppHibernationJob(context, LOG_TAG)
+
+                // Verify that permission is not revoked because the permission was changed
+                // within the unused threshold even though the app itself is unused
+                assertPermission(PERMISSION_GRANTED)
+            }
+        }
+    }
+
+    @AppModeFull(reason = "Uses separate apps for testing")
+    @Test
+    fun testPermissionEventCleanupService_scrubsEvents() {
+        val unusedThreshold = 15_000L
+        withUnusedThresholdMs(unusedThreshold) {
+            withDummyApp {
+                // Setup
+                // Ensure app is considered unused
+                Thread.sleep(unusedThreshold)
+                goToPermissions()
+                click("Calendar")
+                click("Allow")
+                goBack()
+                goBack()
+                goBack()
+                // Run with threshold where events would be cleaned up
+                withUnusedThresholdMs(0) {
+                    runPermissionEventCleanupJob(context)
+                    Thread.sleep(3000L)
+                }
+
+                runAppHibernationJob(context, LOG_TAG)
+
+                // Verify that permission is revoked because there are no recent permission changes
+                assertPermission(PERMISSION_DENIED)
+            }
+        }
+    }
+
+    @AppModeFull(reason = "Uses separate apps for testing")
+    @Test
     fun testPreMinAutoRevokeVersionUnusedApp_doesntGetPermissionRevoked() {
         withUnusedThresholdMs(3L) {
             withDummyApp(preMinVersionApkPath, preMinVersionAppPackageName) {
@@ -436,6 +498,7 @@
             waitFindObject(By.res("com.android.permissioncontroller:id/permission_allow_button"))
                     .click()
         }
+        waitForIdle()
     }
 
     private fun clickUninstallIcon() {
diff --git a/tests/tests/permission/Android.bp b/tests/tests/permission/Android.bp
index eae7276..5c861b5 100644
--- a/tests/tests/permission/Android.bp
+++ b/tests/tests/permission/Android.bp
@@ -47,6 +47,8 @@
         // which provides assertThrows
         "testng",
         "bluetooth-test-util-lib",
+        "CtsAccessibilityCommon",
+        "safety-center-internal-data",
     ],
     jni_libs: [
         "libctspermission_jni",
diff --git a/tests/tests/permission/AndroidManifest.xml b/tests/tests/permission/AndroidManifest.xml
index dc19893..8f3d39b 100644
--- a/tests/tests/permission/AndroidManifest.xml
+++ b/tests/tests/permission/AndroidManifest.xml
@@ -71,6 +71,15 @@
                 <action android:name="android.service.notification.NotificationListenerService"/>
             </intent-filter>
         </service>
+        <service android:name=".AccessibilityTestService"
+            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.accessibilityservice.AccessibilityService"/>
+            </intent-filter>
+            <meta-data android:name="android.accessibilityservice"
+                android:resource="@xml/test_accessibilityservice"/>
+        </service>
     </application>
 
     <!--
diff --git a/tests/tests/permission/AndroidTest.xml b/tests/tests/permission/AndroidTest.xml
index 58ee52f..56a72c9 100644
--- a/tests/tests/permission/AndroidTest.xml
+++ b/tests/tests/permission/AndroidTest.xml
@@ -91,6 +91,7 @@
         <option name="push" value="CtsStorageEscalationApp28.apk->/data/local/tmp/cts/permissions/CtsStorageEscalationApp28.apk" />
         <option name="push" value="CtsStorageEscalationApp29Full.apk->/data/local/tmp/cts/permissions/CtsStorageEscalationApp29Full.apk" />
         <option name="push" value="CtsStorageEscalationApp29Scoped.apk->/data/local/tmp/cts/permissions/CtsStorageEscalationApp29Scoped.apk" />
+        <option name="push" value="CtsAppThatHasNotificationListener.apk->/data/local/tmp/cts/permissions/CtsAppThatHasNotificationListener.apk" />
     </target_preparer>
 
     <!-- Remove additional apps if installed -->
@@ -102,6 +103,7 @@
         <option name="teardown-command" value="pm uninstall android.permission.cts.revokepermissionwhenremoved.userapp" />
         <option name="teardown-command" value="pm uninstall android.permission.cts.revokepermissionwhenremoved.runtimepermissiondefinerapp" />
         <option name="teardown-command" value="pm uninstall android.permission.cts.revokepermissionwhenremoved.runtimepermissionuserapp" />
+        <option name="teardown-command" value="pm uninstall android.permission.cts.appthathasnotificationlistener" />
     </target_preparer>
 
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
diff --git a/tests/tests/permission/AppThatHasNotificationListener/Android.bp b/tests/tests/permission/AppThatHasNotificationListener/Android.bp
new file mode 100644
index 0000000..419ab5d
--- /dev/null
+++ b/tests/tests/permission/AppThatHasNotificationListener/Android.bp
@@ -0,0 +1,39 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "CtsAppThatHasNotificationListener",
+    defaults: [
+        "cts_defaults",
+        "mts-target-sdk-version-current",
+    ],
+    sdk_version: "current",
+    min_sdk_version: "30",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "sts",
+        "mts-permission",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+}
diff --git a/tests/tests/permission/AppThatHasNotificationListener/AndroidManifest.xml b/tests/tests/permission/AppThatHasNotificationListener/AndroidManifest.xml
new file mode 100644
index 0000000..03d23df
--- /dev/null
+++ b/tests/tests/permission/AppThatHasNotificationListener/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.permission.cts.appthathasnotificationlistener"
+          android:versionCode="1">
+
+    <application android:label="CtsNotificationListener">
+        <service
+            android:name=".CtsNotificationListenerService"
+            android:label="CtsNotificationListener"
+            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.service.notification.NotificationListenerService"/>
+            </intent-filter>
+        </service>
+    </application>
+</manifest>
diff --git a/tests/tests/permission/AppThatHasNotificationListener/src/android/permission/cts/appthathasnotificationlistener/CtsNotificationListenerService.java b/tests/tests/permission/AppThatHasNotificationListener/src/android/permission/cts/appthathasnotificationlistener/CtsNotificationListenerService.java
new file mode 100644
index 0000000..2bd423e
--- /dev/null
+++ b/tests/tests/permission/AppThatHasNotificationListener/src/android/permission/cts/appthathasnotificationlistener/CtsNotificationListenerService.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts.appthathasnotificationlistener;
+
+import android.service.notification.NotificationListenerService;
+
+public class CtsNotificationListenerService extends NotificationListenerService {}
diff --git a/tests/tests/permission/permissionTestUtilLib/src/android/permission/cts/TestUtils.java b/tests/tests/permission/permissionTestUtilLib/src/android/permission/cts/TestUtils.java
index dfcc38e..8dd3e96 100644
--- a/tests/tests/permission/permissionTestUtilLib/src/android/permission/cts/TestUtils.java
+++ b/tests/tests/permission/permissionTestUtilLib/src/android/permission/cts/TestUtils.java
@@ -16,9 +16,16 @@
 
 package android.permission.cts;
 
+import android.app.UiAutomation;
+import android.os.Process;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.Assert;
 
 /** Common test utilities */
 public class TestUtils {
@@ -123,4 +130,42 @@
             }
         }
     }
+
+    /**
+     * Run the job and then wait for completion
+     */
+    public static void runJobAndWaitUntilCompleted(
+            String packageName,
+            int jobId, long timeout) {
+        runJobAndWaitUntilCompleted(packageName, jobId, timeout,
+                InstrumentationRegistry.getInstrumentation().getUiAutomation());
+    }
+
+    /**
+     * Run the job and then wait for completion
+     */
+    public static void runJobAndWaitUntilCompleted(
+            String packageName,
+            int jobId,
+            long timeout,
+            UiAutomation automation) {
+        String runJobCmd = "cmd jobscheduler run -u " + Process.myUserHandle().getIdentifier()
+                + " -f " + packageName + " " + jobId;
+        String statusCmd = "cmd jobscheduler get-job-state -u "
+                + Process.myUserHandle().getIdentifier() + " " + packageName + " " + jobId;
+
+        SystemUtil.runWithShellPermissionIdentity(automation, () -> {
+            SystemUtil.runShellCommand(automation, runJobCmd);
+            Thread.sleep(500);
+            try {
+                eventually(() -> Assert.assertEquals(
+                        "The job is probably still running",
+                        "waiting",
+                        SystemUtil.runShellCommand(automation, statusCmd).trim()),
+                        timeout);
+            } catch (Throwable e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
 }
diff --git a/tests/tests/permission/res/xml/test_accessibilityservice.xml b/tests/tests/permission/res/xml/test_accessibilityservice.xml
new file mode 100644
index 0000000..fa87e2e
--- /dev/null
+++ b/tests/tests/permission/res/xml/test_accessibilityservice.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accessibilityEventTypes="typeAllMask"
+    android:accessibilityFeedbackType="feedbackGeneric"
+    android:canRetrieveWindowContent="true"
+    android:accessibilityFlags="flagDefault"
+    android:notificationTimeout="0" />
diff --git a/tests/tests/permission/src/android/permission/cts/AccessibilityPrivacySourceTest.kt b/tests/tests/permission/src/android/permission/cts/AccessibilityPrivacySourceTest.kt
new file mode 100644
index 0000000..58238e3
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/AccessibilityPrivacySourceTest.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.accessibility.cts.common.InstrumentedAccessibilityService
+import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Process
+import android.permission.cts.NotificationListenerUtils.assertEmptyNotification
+import android.permission.cts.NotificationListenerUtils.assertNotificationExist
+import android.permission.cts.NotificationListenerUtils.cancelNotification
+import android.permission.cts.NotificationListenerUtils.cancelNotifications
+import android.permission.cts.NotificationListenerUtils.getNotification
+import android.permission.cts.SafetyCenterUtils.assertSafetyCenterIssueDoesNotExist
+import android.permission.cts.SafetyCenterUtils.assertSafetyCenterIssueExist
+import android.permission.cts.SafetyCenterUtils.assertSafetyCenterStarted
+import android.permission.cts.SafetyCenterUtils.deviceSupportsSafetyCenter
+import android.permission.cts.SafetyCenterUtils.setDeviceConfigPrivacyProperty
+import android.provider.DeviceConfig
+import android.safetycenter.SafetyCenterManager
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.DeviceConfigStateChangerRule
+import com.android.compatibility.common.util.ProtoUtils
+import com.android.compatibility.common.util.SystemUtil.runShellCommand
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.server.job.nano.JobSchedulerServiceDumpProto
+import org.junit.After
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+class AccessibilityPrivacySourceTest {
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = instrumentation.targetContext
+    private val mAccessibilityServiceRule =
+        InstrumentedAccessibilityServiceTestRule(AccessibilityTestService::class.java, false)
+    private val permissionControllerPackage = context.packageManager.permissionControllerPackageName
+    private val accessibilityTestService =
+        ComponentName(context, AccessibilityTestService::class.java).flattenToString()
+    private val safetyCenterIssueId = "accessibility_$accessibilityTestService"
+    private val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java)
+
+    @get:Rule
+    val deviceConfigSafetyCenterEnabled =
+        DeviceConfigStateChangerRule(
+            context, DeviceConfig.NAMESPACE_PRIVACY, SAFETY_CENTER_ENABLED, true.toString())
+
+    @get:Rule
+    val deviceConfigA11ySourceEnabled =
+        DeviceConfigStateChangerRule(
+            context, DeviceConfig.NAMESPACE_PRIVACY, ACCESSIBILITY_SOURCE_ENABLED, true.toString())
+
+    @get:Rule
+    val deviceConfigA11yListenerDisabled =
+        DeviceConfigStateChangerRule(
+            context,
+            DeviceConfig.NAMESPACE_PRIVACY,
+            ACCESSIBILITY_LISTENER_ENABLED,
+            false.toString())
+
+    @Before
+    fun setup() {
+        Assume.assumeTrue(deviceSupportsSafetyCenter(context))
+        InstrumentedAccessibilityService.disableAllServices()
+        runShellCommand("input keyevent KEYCODE_WAKEUP")
+        resetPermissionController()
+        cancelNotifications(permissionControllerPackage)
+    }
+
+    @After
+    fun cleanup() {
+        cancelNotifications(permissionControllerPackage)
+        runWithShellPermissionIdentity { safetyCenterManager?.clearAllSafetySourceDataForTests() }
+    }
+
+    @Test
+    fun testJobSendsNotification() {
+        mAccessibilityServiceRule.enableService()
+        runJobAndWaitUntilCompleted()
+        assertNotificationExist(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+    }
+
+    @Test
+    fun testJobSendsIssuesToSafetyCenter() {
+        mAccessibilityServiceRule.enableService()
+        runJobAndWaitUntilCompleted()
+        assertSafetyCenterIssueExist(
+            SC_ACCESSIBILITY_SOURCE_ID, safetyCenterIssueId, SC_ACCESSIBILITY_ISSUE_TYPE_ID)
+    }
+
+    @Test
+    fun testJobDoesNotSendNotificationInSecondRunForSameService() {
+        mAccessibilityServiceRule.enableService()
+        runJobAndWaitUntilCompleted()
+        assertNotificationExist(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+
+        cancelNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+
+        runJobAndWaitUntilCompleted()
+        assertEmptyNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+    }
+
+    @Test
+    fun testAccessibilityListenerSendsIssueToSafetyCenter() {
+        setDeviceConfigPrivacyProperty(ACCESSIBILITY_LISTENER_ENABLED, true.toString())
+        val automation = getAutomation()
+        mAccessibilityServiceRule.enableService()
+        TestUtils.eventually(
+            {
+                assertSafetyCenterIssueExist(
+                    SC_ACCESSIBILITY_SOURCE_ID,
+                    safetyCenterIssueId,
+                    SC_ACCESSIBILITY_ISSUE_TYPE_ID,
+                    automation)
+            },
+            TIMEOUT_MILLIS)
+        automation.destroy()
+    }
+
+    @Test
+    fun testJobWithDisabledServiceDoesNotSendNotification() {
+        runJobAndWaitUntilCompleted()
+        assertEmptyNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+    }
+
+    @Test
+    fun testJobWithDisabledServiceDoesNotSendIssueToSafetyCenter() {
+        runJobAndWaitUntilCompleted()
+        assertSafetyCenterIssueDoesNotExist(
+            SC_ACCESSIBILITY_SOURCE_ID, safetyCenterIssueId, SC_ACCESSIBILITY_ISSUE_TYPE_ID)
+    }
+
+    @Test
+    fun testJobWithAccessibilityFeatureDisabledDoesNotSendNotification() {
+        setDeviceConfigPrivacyProperty(ACCESSIBILITY_SOURCE_ENABLED, false.toString())
+        mAccessibilityServiceRule.enableService()
+        runJobAndWaitUntilCompleted()
+        assertEmptyNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+    }
+
+    @Test
+    fun testJobWithAccessibilityFeatureDisabledDoesNotSendIssueToSafetyCenter() {
+        setDeviceConfigPrivacyProperty(ACCESSIBILITY_SOURCE_ENABLED, false.toString())
+        mAccessibilityServiceRule.enableService()
+        runJobAndWaitUntilCompleted()
+        assertSafetyCenterIssueDoesNotExist(
+            SC_ACCESSIBILITY_SOURCE_ID, safetyCenterIssueId, SC_ACCESSIBILITY_ISSUE_TYPE_ID)
+    }
+
+    @Test
+    fun testJobWithSafetyCenterDisabledDoesNotSendNotification() {
+        setDeviceConfigPrivacyProperty(SAFETY_CENTER_ENABLED, false.toString())
+        mAccessibilityServiceRule.enableService()
+        runJobAndWaitUntilCompleted()
+        assertEmptyNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+    }
+
+    @Test
+    fun testJobWithSafetyCenterDisabledDoesNotSendIssueToSafetyCenter() {
+        setDeviceConfigPrivacyProperty(SAFETY_CENTER_ENABLED, false.toString())
+        mAccessibilityServiceRule.enableService()
+        runJobAndWaitUntilCompleted()
+        assertSafetyCenterIssueDoesNotExist(
+            SC_ACCESSIBILITY_SOURCE_ID, safetyCenterIssueId, SC_ACCESSIBILITY_ISSUE_TYPE_ID)
+    }
+
+    @Test
+    fun testNotificationClickOpenSafetyCenter() {
+        mAccessibilityServiceRule.enableService()
+        runJobAndWaitUntilCompleted()
+        val statusBarNotification =
+            getNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+        Assert.assertNotNull(statusBarNotification)
+        val contentIntent = statusBarNotification!!.notification.contentIntent
+        contentIntent.send()
+        assertSafetyCenterStarted()
+    }
+
+    private fun getAutomation(): UiAutomation {
+        return instrumentation.getUiAutomation(
+            UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES)
+    }
+
+    private fun runJobAndWaitUntilCompleted() {
+        TestUtils.runJobAndWaitUntilCompleted(
+            permissionControllerPackage, ACCESSIBILITY_JOB_ID, TIMEOUT_MILLIS, getAutomation())
+    }
+
+    /** Reset the permission controllers state. */
+    @Throws(Throwable::class)
+    private fun resetPermissionController() {
+        PermissionUtils.clearAppState(permissionControllerPackage)
+        val currentUserId = Process.myUserHandle().identifier
+
+        // Wait until jobs are cleared
+        TestUtils.eventually(
+            {
+                val dump = getJobSchedulerDump()
+                for (job in dump!!.registeredJobs) {
+                    if (job.dump.sourceUserId == currentUserId &&
+                        job.dump.sourcePackageName == permissionControllerPackage) {
+                        Assert.assertFalse(
+                            job.dump.jobInfo.service.className.contains("AccessibilityJobService"))
+                    }
+                }
+            },
+            TIMEOUT_MILLIS)
+
+        runShellCommand(
+            "cmd jobscheduler reset-execution-quota -u " +
+                "${Process.myUserHandle().identifier} $permissionControllerPackage")
+
+        context.sendBroadcast(
+            Intent().apply {
+                setClassName(permissionControllerPackage, AccessibilityOnBootReceiver)
+                setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+                setPackage(permissionControllerPackage)
+            })
+
+        // Wait until jobs are set up
+        TestUtils.eventually(
+            {
+                val dump = getJobSchedulerDump()
+                for (job in dump!!.registeredJobs) {
+                    if (job.dump.sourceUserId == currentUserId &&
+                        job.dump.sourcePackageName == permissionControllerPackage &&
+                        job.dump.jobInfo.service.className.contains("AccessibilityJobService")) {
+                        return@eventually
+                    }
+                }
+                Assert.fail("accessibility job not found")
+            },
+            TIMEOUT_MILLIS)
+    }
+
+    @Throws(Exception::class)
+    private fun getJobSchedulerDump(): JobSchedulerServiceDumpProto? {
+        return ProtoUtils.getProto(
+            getAutomation(),
+            JobSchedulerServiceDumpProto::class.java,
+            ProtoUtils.DUMPSYS_JOB_SCHEDULER)
+    }
+
+    companion object {
+        private const val SC_ACCESSIBILITY_SOURCE_ID = "AndroidAccessibility"
+        private const val ACCESSIBILITY_SOURCE_ENABLED = "sc_accessibility_source_enabled"
+        private const val SAFETY_CENTER_ENABLED = "safety_center_is_enabled"
+        private const val ACCESSIBILITY_LISTENER_ENABLED = "sc_accessibility_listener_enabled"
+
+        private const val ACCESSIBILITY_JOB_ID = 6
+        private const val ACCESSIBILITY_NOTIFICATION_ID = 4
+        private const val TIMEOUT_MILLIS: Long = 10000
+
+        private const val SC_ACCESSIBILITY_ISSUE_TYPE_ID = "accessibility_privacy_issue"
+
+        private const val AccessibilityOnBootReceiver =
+            "com.android.permissioncontroller.privacysources.AccessibilityOnBootReceiver"
+
+        @get:ClassRule
+        @JvmStatic
+        val ctsNotificationListenerHelper =
+            CtsNotificationListenerHelperRule(
+                InstrumentationRegistry.getInstrumentation().targetContext)
+    }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/AccessibilityTestService.kt b/tests/tests/permission/src/android/permission/cts/AccessibilityTestService.kt
new file mode 100644
index 0000000..9f5e3f1
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/AccessibilityTestService.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.accessibility.cts.common.InstrumentedAccessibilityService
+
+/**
+ * Test Accessibility Service
+ */
+class AccessibilityTestService : InstrumentedAccessibilityService()
diff --git a/tests/tests/permission/src/android/permission/cts/BaseNotificationListenerCheckTest.java b/tests/tests/permission/src/android/permission/cts/BaseNotificationListenerCheckTest.java
new file mode 100644
index 0000000..6ce52b1
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/BaseNotificationListenerCheckTest.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts;
+
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.content.Intent.ACTION_BOOT_COMPLETED;
+import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
+import static android.os.Process.myUserHandle;
+import static android.permission.cts.PermissionUtils.clearAppState;
+import static android.permission.cts.TestUtils.eventually;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.server.job.nano.JobPackageHistoryProto.START_PERIODIC_JOB;
+import static com.android.server.job.nano.JobPackageHistoryProto.STOP_PERIODIC_JOB;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import static java.lang.Math.max;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.app.NotificationManager;
+import android.app.UiAutomation;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+import android.provider.DeviceConfig;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
+import com.android.compatibility.common.util.ProtoUtils;
+import com.android.server.job.nano.JobPackageHistoryProto;
+import com.android.server.job.nano.JobSchedulerServiceDumpProto;
+import com.android.server.job.nano.JobSchedulerServiceDumpProto.RegisteredJob;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/**
+ * Base test class used for {@code NotificationListenerCheckTest} and
+ * {@code NotificationListenerCheckWithSafetyCenterUnsupportedTest}
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Cannot set system settings as instant app. Also we never show a notification"
+        + " listener check notification for instant apps.")
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+public class BaseNotificationListenerCheckTest {
+    private static final String LOG_TAG = BaseNotificationListenerCheckTest.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    protected static final String TEST_APP_PKG =
+            "android.permission.cts.appthathasnotificationlistener";
+    private static final String TEST_APP_NOTIFICATION_SERVICE =
+            TEST_APP_PKG + ".CtsNotificationListenerService";
+    protected static final String TEST_APP_NOTIFICATION_LISTENER_APK =
+            "/data/local/tmp/cts/permissions/CtsAppThatHasNotificationListener.apk";
+
+    private static final int NOTIFICATION_LISTENER_CHECK_JOB_ID = 4;
+
+    /**
+     * Device config property for whether notification listener check is enabled on the device
+     */
+    private static final String PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED =
+            "notification_listener_check_enabled";
+
+    /**
+     * Device config property for time period in milliseconds after which current enabled
+     * notification
+     * listeners are queried
+     */
+    private static final String PROPERTY_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS =
+            "notification_listener_check_interval_millis";
+
+    private static final Long OVERRIDE_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS =
+            SECONDS.toMillis(1);
+
+    private static final String PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW =
+            "qc_max_job_count_per_rate_limiting_window";
+
+    private static final String PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS =
+            "qc_rate_limiting_window_ms";
+
+    /**
+     * ID for notification shown by
+     * {@link com.android.permissioncontroller.privacysources.NotificationListenerCheck}.
+     */
+    public static final int NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID = 3;
+
+    protected static final long UNEXPECTED_TIMEOUT_MILLIS = 10000;
+    protected static final long ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS = 5000;
+
+    private static final Context sContext = InstrumentationRegistry.getTargetContext();
+    private static final PackageManager sPackageManager = sContext.getPackageManager();
+    private static final UiAutomation sUiAutomation = InstrumentationRegistry.getInstrumentation()
+            .getUiAutomation();
+
+    private static final String PERMISSION_CONTROLLER_PKG = sContext.getPackageManager()
+            .getPermissionControllerPackageName();
+
+    private static List<ComponentName> sPreviouslyEnabledNotificationListeners;
+
+    // Override SafetyCenter enabled flag
+    @Rule
+    public DeviceConfigStateChangerRule sPrivacyDeviceConfigSafetyCenterEnabled =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    SafetyCenterUtils.PROPERTY_SAFETY_CENTER_ENABLED,
+                    Boolean.toString(true));
+
+    // Override NlsCheck enabled flag
+    @Rule
+    public DeviceConfigStateChangerRule sPrivacyDeviceConfigNlsCheckEnabled =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED,
+                    Boolean.toString(true));
+
+    // Override general notification interval from once every day to once ever 1 second
+    @Rule
+    public DeviceConfigStateChangerRule sPrivacyDeviceConfigNlsCheckIntervalMillis =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    PROPERTY_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS,
+                    Long.toString(OVERRIDE_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS));
+
+    // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
+    @Rule
+    public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig1 =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW,
+                    Integer.toString(3000000));
+
+    // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
+    @Rule
+    public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig2 =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS,
+                    Integer.toString(30000));
+
+    @Rule
+    public CtsNotificationListenerHelperRule ctsNotificationListenerHelper =
+            new CtsNotificationListenerHelperRule(sContext);
+
+    @BeforeClass
+    public static void beforeClassSetup() throws Exception {
+        // Disallow any OEM enabled NLS
+        disallowPreexistingNotificationListeners();
+    }
+
+    @AfterClass
+    public static void afterClassTearDown() throws Throwable {
+        // Reallow any previously OEM allowed NLS
+        reallowPreexistingNotificationListeners();
+    }
+
+    private static void setDeviceConfigPrivacyProperty(String propertyName, String value) {
+        runWithShellPermissionIdentity(() -> {
+            boolean valueWasSet =  DeviceConfig.setProperty(
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    /* name = */ propertyName,
+                    /* value = */ value,
+                    /* makeDefault = */ false);
+            if (!valueWasSet) {
+                throw new  IllegalStateException("Could not set " + propertyName + " to " + value);
+            }
+        }, WRITE_DEVICE_CONFIG);
+    }
+
+    /**
+     * Enable or disable notification listener check
+     */
+    protected static void setNotificationListenerCheckEnabled(boolean enabled) {
+        setDeviceConfigPrivacyProperty(
+                PROPERTY_NOTIFICATION_LISTENER_CHECK_ENABLED,
+                /* value = */ String.valueOf(enabled));
+    }
+
+    /**
+     * Allow or disallow a {@link NotificationListenerService} component for the current user
+     *
+     * @param listenerComponent {@link NotificationListenerService} component to allow or disallow
+     */
+    private static void setNotificationListenerServiceAllowed(ComponentName listenerComponent,
+            boolean allowed) {
+        String command = " cmd notification " + (allowed ? "allow_listener " : "disallow_listener ")
+                + listenerComponent.flattenToString();
+        runShellCommand(command);
+    }
+
+    private static void disallowPreexistingNotificationListeners() {
+        runWithShellPermissionIdentity(() -> {
+            NotificationManager notificationManager =
+                    sContext.getSystemService(NotificationManager.class);
+            sPreviouslyEnabledNotificationListeners =
+                    notificationManager.getEnabledNotificationListeners();
+        });
+        if (DEBUG) {
+            Log.d(LOG_TAG, "Found " + sPreviouslyEnabledNotificationListeners.size()
+                    + " previously allowed notification listeners. Disabling before test run.");
+        }
+        for (ComponentName listener : sPreviouslyEnabledNotificationListeners) {
+            setNotificationListenerServiceAllowed(listener, false);
+        }
+    }
+
+    private static void reallowPreexistingNotificationListeners() {
+        if (DEBUG) {
+            Log.d(LOG_TAG, "Re-allowing " + sPreviouslyEnabledNotificationListeners.size()
+                    + " previously allowed notification listeners found before test run.");
+        }
+        for (ComponentName listener : sPreviouslyEnabledNotificationListeners) {
+            setNotificationListenerServiceAllowed(listener, true);
+        }
+    }
+
+    protected void allowTestAppNotificationListenerService() {
+        setNotificationListenerServiceAllowed(
+                new ComponentName(TEST_APP_PKG, TEST_APP_NOTIFICATION_SERVICE), true);
+    }
+
+    protected void disallowTestAppNotificationListenerService() {
+        setNotificationListenerServiceAllowed(
+                new ComponentName(TEST_APP_PKG, TEST_APP_NOTIFICATION_SERVICE), false);
+    }
+
+    /**
+     * Get the state of the job scheduler
+     */
+    private static JobSchedulerServiceDumpProto getJobSchedulerDump() throws Exception {
+        return ProtoUtils.getProto(sUiAutomation, JobSchedulerServiceDumpProto.class,
+                ProtoUtils.DUMPSYS_JOB_SCHEDULER);
+    }
+
+    /**
+     * Get the last time the NOTIFICATION_LISTENER_CHECK_JOB_ID job was started/stopped for
+     * permission
+     * controller.
+     *
+     * @param event the job event (start/stop)
+     * @return the last time the event happened.
+     */
+    private static long getLastJobTime(int event) throws Exception {
+        int permControllerUid = sPackageManager.getPackageUid(PERMISSION_CONTROLLER_PKG, 0);
+
+        long lastTime = -1;
+
+        for (JobPackageHistoryProto.HistoryEvent historyEvent :
+                getJobSchedulerDump().history.historyEvent) {
+            if (historyEvent.uid == permControllerUid
+                    && historyEvent.jobId == NOTIFICATION_LISTENER_CHECK_JOB_ID
+                    && historyEvent.event == event) {
+                lastTime = max(lastTime,
+                        System.currentTimeMillis() - historyEvent.timeSinceEventMs);
+            }
+        }
+
+        return lastTime;
+    }
+
+    /**
+     * Force a run of the notification listener check.
+     */
+    protected static void runNotificationListenerCheck() throws Throwable {
+        // Sleep a little to make sure we don't have overlap in timing
+        Thread.sleep(1000);
+
+        long beforeJob = System.currentTimeMillis();
+
+        // Sleep a little to avoid raciness in time keeping
+        Thread.sleep(1000);
+
+        runShellCommand("cmd jobscheduler run -u " + myUserHandle().getIdentifier() + " -f "
+                + PERMISSION_CONTROLLER_PKG + " " + NOTIFICATION_LISTENER_CHECK_JOB_ID);
+
+        eventually(() -> {
+            long startTime = getLastJobTime(START_PERIODIC_JOB);
+            assertTrue(startTime + " !> " + beforeJob, startTime > beforeJob);
+        }, UNEXPECTED_TIMEOUT_MILLIS);
+
+        // We can't simply require startTime <= endTime because the time being reported isn't
+        // accurate, and sometimes the end time may come before the start time by around 100 ms.
+        eventually(() -> {
+            long stopTime = getLastJobTime(STOP_PERIODIC_JOB);
+            assertTrue(stopTime + " !> " + beforeJob, stopTime > beforeJob);
+        }, UNEXPECTED_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Skip tests for if Safety Center not supported
+     */
+    protected void assumeDeviceSupportsSafetyCenter() {
+        assumeTrue(SafetyCenterUtils.deviceSupportsSafetyCenter(sContext));
+    }
+
+    /**
+     * Skip tests for if Safety Center IS supported
+     */
+    protected void assumeDeviceDoesNotSupportSafetyCenter() {
+        assumeFalse(SafetyCenterUtils.deviceSupportsSafetyCenter(sContext));
+    }
+
+    protected void wakeUpAndDismissKeyguard() {
+        runShellCommand("input keyevent KEYCODE_WAKEUP");
+        runShellCommand("wm dismiss-keyguard");
+    }
+
+    /**
+     * Reset the permission controllers state before each test
+     */
+    protected void resetPermissionControllerBeforeEachTest() throws Throwable {
+        resetPermissionController();
+
+        // ensure no posted notification listener notifications exits
+        eventually(() -> assertNull(getNotification(false)), UNEXPECTED_TIMEOUT_MILLIS);
+
+        // Reset job scheduler stats (to allow more jobs to be run)
+        runShellCommand(
+                "cmd jobscheduler reset-execution-quota -u " + myUserHandle().getIdentifier() + " "
+                        + PERMISSION_CONTROLLER_PKG);
+    }
+
+    /**
+     * Reset the permission controllers state.
+     */
+    private static void resetPermissionController() throws Throwable {
+        clearAppState(PERMISSION_CONTROLLER_PKG);
+        int currentUserId = myUserHandle().getIdentifier();
+
+        // Wait until jobs are cleared
+        eventually(() -> {
+            JobSchedulerServiceDumpProto dump = getJobSchedulerDump();
+
+            for (RegisteredJob job : dump.registeredJobs) {
+                if (job.dump.sourceUserId == currentUserId) {
+                    assertNotEquals(job.dump.sourcePackageName, PERMISSION_CONTROLLER_PKG);
+                }
+            }
+        }, UNEXPECTED_TIMEOUT_MILLIS);
+
+        // Setup up permission controller again (simulate a reboot)
+        Intent permissionControllerSetupIntent = null;
+        for (ResolveInfo ri : sContext.getPackageManager().queryBroadcastReceivers(
+                new Intent(ACTION_BOOT_COMPLETED), 0)) {
+            String pkg = ri.activityInfo.packageName;
+
+            if (pkg.equals(PERMISSION_CONTROLLER_PKG)) {
+                permissionControllerSetupIntent = new Intent()
+                        .setClassName(pkg, ri.activityInfo.name)
+                        .setFlags(FLAG_RECEIVER_FOREGROUND)
+                        .setPackage(PERMISSION_CONTROLLER_PKG);
+
+                sContext.sendBroadcast(permissionControllerSetupIntent);
+            }
+        }
+
+        // Wait until jobs are set up
+        eventually(() -> {
+            JobSchedulerServiceDumpProto dump = getJobSchedulerDump();
+
+            for (RegisteredJob job : dump.registeredJobs) {
+                if (job.dump.sourceUserId == currentUserId
+                        && job.dump.sourcePackageName.equals(PERMISSION_CONTROLLER_PKG)
+                        && job.dump.jobInfo.service.className.contains(
+                        "NotificationListenerCheck")) {
+                    return;
+                }
+            }
+
+            fail("Permission controller jobs not found");
+        }, UNEXPECTED_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Preshow/dismiss cts NotificationListener notification as it negatively affects test results
+     * (can result in unexpected test pass/failures)
+     */
+    protected void triggerAndDismissCtsNotificationListenerNotification() throws Throwable {
+        // CtsNotificationListenerService isn't enabled at this point, but NotificationListener
+        // should be. Mark as notified by showing and dismissing
+        runNotificationListenerCheck();
+
+        // Ensure notification shows and dismiss
+        eventually(() -> assertNotNull(getNotification(true)),
+                UNEXPECTED_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Get a notification listener notification that is currently visible.
+     *
+     * @param cancelNotification if `true` the notification is canceled inside this method
+     * @return The notification or `null` if there is none
+     */
+    protected StatusBarNotification getNotification(boolean cancelNotification) throws Throwable {
+        return NotificationUtils.getNotificationForPackageAndId(
+                PERMISSION_CONTROLLER_PKG,
+                NOTIFICATION_LISTENER_CHECK_NOTIFICATION_ID,
+                cancelNotification);
+    }
+
+    /**
+     * Clear any notifications related to NotificationListenerCheck to ensure clean test setup
+     */
+    protected void clearNotifications() throws Throwable {
+        // Clear notification if present
+        NotificationUtils.clearNotificationsForPackage(PERMISSION_CONTROLLER_PKG);
+    }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/CtsNotificationListenerHelperRule.kt b/tests/tests/permission/src/android/permission/cts/CtsNotificationListenerHelperRule.kt
new file mode 100644
index 0000000..b7fa960
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/CtsNotificationListenerHelperRule.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.content.ComponentName
+import android.content.Context
+import com.android.compatibility.common.util.SystemUtil
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Rule that enables and disables the CTS NotificationListenerService
+ */
+class CtsNotificationListenerHelperRule(context: Context) : TestRule {
+
+    private val notificationListenerComponentName = ComponentName(
+        context,
+        NotificationListener::class.java
+    )
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                try {
+                    // Allow NLS used to verify notifications sent
+                    SystemUtil.runShellCommand(ALLOW_NLS_COMMAND +
+                        notificationListenerComponentName.flattenToString())
+
+                    base.evaluate()
+                } finally {
+                    // Disallow NLS used to verify notifications sent
+                    SystemUtil.runShellCommand(DISALLOW_NLS_COMMAND +
+                        notificationListenerComponentName.flattenToString())
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val ALLOW_NLS_COMMAND = "cmd notification allow_listener "
+        private const val DISALLOW_NLS_COMMAND = "cmd notification disallow_listener "
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java b/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
index ab7a15d..b41fb08 100644
--- a/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
+++ b/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
@@ -18,9 +18,9 @@
 
 import static android.Manifest.permission.ACCESS_BACKGROUND_LOCATION;
 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
 import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
 import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
-import static android.app.Notification.EXTRA_TITLE;
 import static android.content.Context.BIND_AUTO_CREATE;
 import static android.content.Context.BIND_NOT_FOREGROUND;
 import static android.content.Intent.ACTION_BOOT_COMPLETED;
@@ -50,6 +50,7 @@
 
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
+import android.app.PendingIntent;
 import android.app.UiAutomation;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -62,6 +63,7 @@
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Looper;
@@ -78,9 +80,10 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.compatibility.common.util.DeviceConfigStateHelper;
+import com.android.compatibility.common.util.DeviceConfigStateChangerRule;
 import com.android.compatibility.common.util.ProtoUtils;
 import com.android.compatibility.common.util.mainline.MainlineModule;
 import com.android.compatibility.common.util.mainline.ModuleDetector;
@@ -93,6 +96,7 @@
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -117,10 +121,22 @@
     private static final String TEST_APP_LOCATION_FG_ACCESS_APK =
             "/data/local/tmp/cts/permissions/AppThatDoesNotHaveBgLocationAccess.apk";
     private static final int LOCATION_ACCESS_CHECK_JOB_ID = 0;
+    private static final int LOCATION_ACCESS_CHECK_NOTIFICATION_ID = 0;
 
-    /** Whether to show location access check notifications. */
+    /**
+     * Whether to show location access check notifications.
+     */
     private static final String PROPERTY_LOCATION_ACCESS_CHECK_ENABLED =
             "location_access_check_enabled";
+    private static final String PROPERTY_LOCATION_ACCESS_CHECK_DELAY_MILLIS =
+            "location_access_check_delay_millis";
+    private static final String PROPERTY_LOCATION_ACCESS_PERIODIC_INTERVAL_MILLIS =
+            "location_access_check_periodic_interval_millis";
+    private static final String PROPERTY_BG_LOCATION_CHECK_ENABLED = "bg_location_check_is_enabled";
+    private static final String PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW =
+            "qc_max_job_count_per_rate_limiting_window";
+    private static final String PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS =
+            "qc_rate_limiting_window_ms";
 
     private static final long UNEXPECTED_TIMEOUT_MILLIS = 10000;
     private static final long EXPECTED_TIMEOUT_MILLIS = 15000;
@@ -147,16 +163,93 @@
     private static ServiceConnection sConnection;
     private static IAccessLocationOnCommand sLocationAccessor;
 
-    private DeviceConfigStateHelper mPrivacyDeviceConfig =
-            new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_PRIVACY);
-    private static DeviceConfigStateHelper sJobSchedulerDeviceConfig =
-            new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
-
     private static void assumeNotPlayManaged() throws Exception {
         assumeFalse(ModuleDetector.moduleIsPlayManaged(
                 sContext.getPackageManager(), MainlineModule.PERMISSION_CONTROLLER));
     }
 
+    // Override location access check flag
+    @Rule
+    public DeviceConfigStateChangerRule mPrivacyDeviceConfig =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    PROPERTY_LOCATION_ACCESS_CHECK_ENABLED,
+                    Boolean.toString(true));
+
+    // Override SafetyCenter enabled flag
+    @Rule
+    public DeviceConfigStateChangerRule sPrivacyDeviceConfigSafetyCenterEnabled =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    SafetyCenterUtils.PROPERTY_SAFETY_CENTER_ENABLED,
+                    Boolean.toString(true));
+
+    // Override BG location enabled flag
+    @Rule
+    public DeviceConfigStateChangerRule sPrivacyDeviceConfigBgLocationCheckEnabled =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    PROPERTY_BG_LOCATION_CHECK_ENABLED,
+                    Boolean.toString(true));
+
+    // Override general notification interval
+    @Rule
+    public DeviceConfigStateChangerRule sPrivacyDeviceConfigBgCheckIntervalMillis =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    PROPERTY_LOCATION_ACCESS_PERIODIC_INTERVAL_MILLIS,
+                    "100");
+
+    // Override general delay interval
+    @Rule
+    public DeviceConfigStateChangerRule sPrivacyDeviceConfigBgCheckDelayMillis =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    PROPERTY_LOCATION_ACCESS_CHECK_DELAY_MILLIS,
+                    "50");
+
+    // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
+    @Rule
+    public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig1 =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    PROPERTY_JOB_SCHEDULER_MAX_JOB_PER_RATE_LIMIT_WINDOW,
+                    Integer.toString(3000000));
+
+    // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
+    @Rule
+    public DeviceConfigStateChangerRule sJobSchedulerDeviceConfig2 =
+            new DeviceConfigStateChangerRule(sContext,
+                    DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    PROPERTY_JOB_SCHEDULER_RATE_LIMIT_WINDOW_MILLIS,
+                    Integer.toString(30000));
+
+    /**
+     * Change settings so that permission controller can show location access notifications more
+     * often.
+     */
+    @BeforeClass
+    public static void reduceDelays() {
+        runWithShellPermissionIdentity(() -> {
+            ContentResolver cr = sContext.getContentResolver();
+            // New settings will be applied in when permission controller is reset
+            Settings.Secure.putLong(cr, LOCATION_ACCESS_CHECK_INTERVAL_MILLIS, 100);
+            Settings.Secure.putLong(cr, LOCATION_ACCESS_CHECK_DELAY_MILLIS, 50);
+        });
+    }
+
+    /**
+     * Reset settings so that permission controller runs normally.
+     */
+    @AfterClass
+    public static void resetDelays() throws Throwable {
+        runWithShellPermissionIdentity(() -> {
+            ContentResolver cr = sContext.getContentResolver();
+            Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_INTERVAL_MILLIS);
+            Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_DELAY_MILLIS);
+        });
+    }
+
     /**
      * Connected to {@value #TEST_APP_PKG} and make it access the location in the background
      */
@@ -272,7 +365,6 @@
      * controller.
      *
      * @param event the job event (start/stop)
-     *
      * @return the last time the event happened.
      */
     private static long getLastJobTime(int event) throws Exception {
@@ -358,8 +450,7 @@
             return null;
         }
 
-        if (notification.getNotification().extras.getString(EXTRA_TITLE, "")
-                .contains(TEST_APP_LABEL)) {
+        if (notification.getId() == LOCATION_ACCESS_CHECK_NOTIFICATION_ID) {
             if (cancelNotification) {
                 notificationService.cancelNotification(notification.getKey());
 
@@ -395,25 +486,6 @@
                 NotificationListener.class).flattenToString()));
     }
 
-    /**
-     * Change settings so that permission controller can show location access notifications more
-     * often.
-     */
-    @BeforeClass
-    public static void reduceDelays() {
-        runWithShellPermissionIdentity(() -> {
-            ContentResolver cr = sContext.getContentResolver();
-
-            // New settings will be applied in when permission controller is reset
-            Settings.Secure.putLong(cr, LOCATION_ACCESS_CHECK_INTERVAL_MILLIS, 100);
-            Settings.Secure.putLong(cr, LOCATION_ACCESS_CHECK_DELAY_MILLIS, 50);
-
-            // Disable job scheduler throttling by allowing 300000 jobs per 30 sec
-            sJobSchedulerDeviceConfig.set("qc_max_job_count_per_rate_limiting_window", "3000000");
-            sJobSchedulerDeviceConfig.set("qc_rate_limiting_window_ms", "30000");
-        });
-    }
-
     @BeforeClass
     public static void installBackgroundAccessApp() throws Exception {
         installBackgroundAccessApp(false);
@@ -444,6 +516,21 @@
         sLocationAccessor = null;
     }
 
+    private void setDeviceConfigProperty(
+            @NonNull String propertyName,
+            @NonNull String value) {
+        runWithShellPermissionIdentity(() -> {
+            boolean valueWasSet = DeviceConfig.setProperty(
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    propertyName,
+                    value,
+                    false);
+            if (!valueWasSet) {
+                throw new IllegalStateException("Could not set " + propertyName + " to " + value);
+            }
+        }, WRITE_DEVICE_CONFIG);
+    }
+
 
     private static void installForegroundAccessApp() throws Exception {
         unbindService();
@@ -514,8 +601,8 @@
      * Enable location access check
      */
     public void enableLocationAccessCheck() throws Throwable {
-        mPrivacyDeviceConfig.set(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED, "true");
-
+        setDeviceConfigProperty(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED,
+                "true");
         // Run a location access check to update enabled state inside permission controller
         runLocationCheck();
     }
@@ -524,8 +611,8 @@
      * Disable location access check
      */
     private void disableLocationAccessCheck() throws Throwable {
-        mPrivacyDeviceConfig.set(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED, "false");
-
+        setDeviceConfigProperty(PROPERTY_LOCATION_ACCESS_CHECK_ENABLED,
+                "false");
         // Run a location access check to update enabled state inside permission controller
         runLocationCheck();
     }
@@ -631,27 +718,10 @@
     }
 
     /**
-     * Reset settings so that permission controller runs normally.
-     */
-    @AfterClass
-    public static void resetDelays() throws Throwable {
-        runWithShellPermissionIdentity(() -> {
-            ContentResolver cr = sContext.getContentResolver();
-
-            Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_INTERVAL_MILLIS);
-            Settings.Secure.resetToDefaults(cr, LOCATION_ACCESS_CHECK_DELAY_MILLIS);
-
-            sJobSchedulerDeviceConfig.restoreOriginalValues();
-        });
-    }
-
-    /**
      * Reset location access check
      */
     @After
     public void resetPrivacyConfig() throws Throwable {
-        mPrivacyDeviceConfig.restoreOriginalValues();
-
         // Run a location access check to update enabled state inside permission controller
         runLocationCheck();
     }
@@ -665,7 +735,6 @@
     public void notificationIsShown() throws Throwable {
         accessLocation();
         runLocationCheck();
-
         eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
     }
 
@@ -844,4 +913,23 @@
         runLocationCheck();
         assertNull(getNotification(false));
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+    public void notificationOnClickOpensSafetyCenter() throws Throwable {
+        accessLocation();
+        runLocationCheck();
+
+        StatusBarNotification currentNotification = eventually(() -> {
+            StatusBarNotification notification = getNotification(false);
+            assertNotNull(notification);
+            return notification;
+        }, EXPECTED_TIMEOUT_MILLIS);
+
+        // Verify content intent
+        PendingIntent contentIntent = currentNotification.getNotification().contentIntent;
+        contentIntent.send();
+
+        SafetyCenterUtils.assertSafetyCenterStarted();
+    }
 }
diff --git a/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckTest.java b/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckTest.java
new file mode 100644
index 0000000..3ee161f
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts;
+
+import static android.permission.cts.PermissionUtils.clearAppState;
+import static android.permission.cts.PermissionUtils.install;
+import static android.permission.cts.PermissionUtils.uninstallApp;
+import static android.permission.cts.TestUtils.ensure;
+import static android.permission.cts.TestUtils.eventually;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.app.PendingIntent;
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+import android.service.notification.StatusBarNotification;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests the {@code NotificationListenerCheck} in permission controller.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Cannot set system settings as instant app. Also we never show a notification"
+        + " listener check notification for instant apps.")
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+public class NotificationListenerCheckTest extends BaseNotificationListenerCheckTest {
+
+    @Before
+    public void setup() throws Throwable {
+        // Skip tests if safety center not allowed
+        assumeDeviceSupportsSafetyCenter();
+
+        wakeUpAndDismissKeyguard();
+        resetPermissionControllerBeforeEachTest();
+
+        // Cts NLS is required to verify sent Notifications, however, we don't want it to show up in
+        // testing
+        triggerAndDismissCtsNotificationListenerNotification();
+
+        clearNotifications();
+
+        // Install and allow the app with NLS for testing
+        install(TEST_APP_NOTIFICATION_LISTENER_APK);
+        allowTestAppNotificationListenerService();
+    }
+
+    @After
+    public void tearDown() throws Throwable {
+        // Disallow and uninstall the app with NLS for testing
+        disallowTestAppNotificationListenerService();
+        uninstallApp(TEST_APP_PKG);
+
+        clearNotifications();
+    }
+
+    @Test
+    public void noNotificationIfFeatureDisabled() throws Throwable {
+        setNotificationListenerCheckEnabled(false);
+
+        runNotificationListenerCheck();
+
+        ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+                ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void noNotificationIfSafetyCenterDisabled() throws Throwable {
+        SafetyCenterUtils.setSafetyCenterEnabled(false);
+
+        runNotificationListenerCheck();
+
+        ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+                ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void notificationIsShown() throws Throwable {
+        runNotificationListenerCheck();
+
+        eventually(() -> assertNotNull("Expected notification, none found", getNotification(false)),
+                UNEXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void notificationIsShownOnlyOnce() throws Throwable {
+        runNotificationListenerCheck();
+        eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+
+        runNotificationListenerCheck();
+
+        ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+                ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void notificationIsShownAgainAfterClear() throws Throwable {
+        runNotificationListenerCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+
+        clearAppState(TEST_APP_PKG);
+
+        // Wait until package is cleared and permission controller has cleared the state
+        Thread.sleep(2000);
+
+        allowTestAppNotificationListenerService();
+        runNotificationListenerCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void notificationIsShownAgainAfterUninstallAndReinstall() throws Throwable {
+        runNotificationListenerCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+
+        uninstallApp(TEST_APP_PKG);
+
+        // Wait until package permission controller has cleared the state
+        Thread.sleep(2000);
+
+        install(TEST_APP_NOTIFICATION_LISTENER_APK);
+
+        allowTestAppNotificationListenerService();
+        runNotificationListenerCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void removeNotificationOnUninstall() throws Throwable {
+        runNotificationListenerCheck();
+
+        eventually(() -> assertNotNull(getNotification(false)), UNEXPECTED_TIMEOUT_MILLIS);
+
+        uninstallApp(TEST_APP_PKG);
+
+        // Wait until package permission controller has cleared the state
+        Thread.sleep(2000);
+
+        eventually(() -> assertNull(getNotification(false)), UNEXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void notificationIsNotShownAfterDisableAppNotificationListener() throws Throwable {
+        disallowTestAppNotificationListenerService();
+
+        runNotificationListenerCheck();
+
+        // We don't expect a notification, but try to trigger one anyway
+        ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+                ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void notificationOnClick_opensSafetyCenter() throws Throwable {
+        runNotificationListenerCheck();
+
+        StatusBarNotification currentNotification = eventually(
+                () -> {
+                    StatusBarNotification notification = getNotification(false);
+                    assertNotNull(notification);
+                    return notification;
+                }, UNEXPECTED_TIMEOUT_MILLIS);
+
+        // Verify content intent
+        PendingIntent contentIntent = currentNotification.getNotification().contentIntent;
+        contentIntent.send();
+
+        SafetyCenterUtils.assertSafetyCenterStarted();
+    }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckWithSafetyCenterUnsupportedTest.java b/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckWithSafetyCenterUnsupportedTest.java
new file mode 100644
index 0000000..a346de6
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckWithSafetyCenterUnsupportedTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts;
+
+import static android.permission.cts.PermissionUtils.install;
+import static android.permission.cts.PermissionUtils.uninstallApp;
+import static android.permission.cts.TestUtils.ensure;
+
+import static org.junit.Assert.assertNull;
+
+import android.os.Build;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests the {@code NotificationListenerCheck} in permission controller.
+ */
+@RunWith(AndroidJUnit4.class)
+@AppModeFull(reason = "Cannot set system settings as instant app. Also we never show a notification"
+        + " listener check notification for instant apps.")
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+public class NotificationListenerCheckWithSafetyCenterUnsupportedTest
+        extends BaseNotificationListenerCheckTest  {
+
+    @Before
+    public void setup() throws Throwable {
+        // Skip tests if safety center is supported
+        assumeDeviceDoesNotSupportSafetyCenter();
+
+        wakeUpAndDismissKeyguard();
+        resetPermissionControllerBeforeEachTest();
+
+        clearNotifications();
+
+        // Install and allow the app with NLS for testing
+        install(TEST_APP_NOTIFICATION_LISTENER_APK);
+        allowTestAppNotificationListenerService();
+    }
+
+    @After
+    public void tearDown() throws Throwable {
+        // Disallow and uninstall the app with NLS for testing
+        disallowTestAppNotificationListenerService();
+        uninstallApp(TEST_APP_PKG);
+
+        clearNotifications();
+    }
+
+    @Test
+    public void noNotifications_featureEnabled_safetyCenterEnabled() throws Throwable {
+        runNotificationListenerCheck();
+
+        ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+                ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void noNotifications_featureDisabled_safetyCenterEnabled() throws Throwable {
+        setNotificationListenerCheckEnabled(false);
+
+        runNotificationListenerCheck();
+
+        ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+                ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void noNotifications_featureEnabled_safetyCenterDisabled() throws Throwable {
+        SafetyCenterUtils.setSafetyCenterEnabled(false);
+
+        runNotificationListenerCheck();
+
+        ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+                ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+    }
+
+    @Test
+    public void noNotifications_featureDisabled_safetyCenterDisabled() throws Throwable {
+        setNotificationListenerCheckEnabled(false);
+        SafetyCenterUtils.setSafetyCenterEnabled(false);
+
+        runNotificationListenerCheck();
+
+        ensure(() -> assertNull("Expected no notifications", getNotification(false)),
+                ENSURE_NOTIFICATION_NOT_SHOWN_EXPECTED_TIMEOUT_MILLIS);
+    }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/NotificationListenerUtils.kt b/tests/tests/permission/src/android/permission/cts/NotificationListenerUtils.kt
new file mode 100644
index 0000000..0ee8a3d
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationListenerUtils.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.service.notification.StatusBarNotification
+import org.junit.Assert
+import android.permission.cts.TestUtils.ensure
+import android.permission.cts.TestUtils.eventually
+
+object NotificationListenerUtils {
+
+    private const val NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS = 5000L
+    private const val NOTIFICATION_WAIT_MILLIS = 2000L
+    private val notificationService = NotificationListener.getInstance()
+
+    @JvmStatic
+    fun assertEmptyNotification(packageName: String, notificationId: Int) {
+        ensure({
+            Assert.assertNull(
+            "Expected no notification",
+            getNotification(packageName, notificationId))
+        }, NOTIFICATION_WAIT_MILLIS)
+    }
+
+    @JvmStatic
+    fun assertNotificationExist(packageName: String, notificationId: Int) {
+        eventually({
+            Assert.assertNotNull(
+                "Expected notification, none found",
+                getNotification(packageName, notificationId))
+        }, NOTIFICATION_WAIT_MILLIS)
+    }
+
+    @JvmStatic
+    fun cancelNotification(packageName: String, notificationId: Int) {
+        val notification = getNotification(packageName, notificationId)
+        if (notification != null) {
+            notificationService.cancelNotification(notification.key)
+            eventually({
+                Assert.assertTrue(getNotification(packageName, notificationId) == null)
+            }, NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS)
+        }
+    }
+
+    @JvmStatic
+    fun cancelNotifications(packageName: String) {
+        val notifications = getNotifications(packageName)
+        if (notifications.isNotEmpty()) {
+            notifications.forEach { notification ->
+                notificationService.cancelNotification(notification.key)
+            }
+            eventually({
+                Assert.assertTrue(getNotifications(packageName).isEmpty())
+            }, NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS)
+        }
+    }
+
+    @JvmStatic
+    fun getNotification(packageName: String, notificationId: Int): StatusBarNotification? {
+        return getNotifications(packageName).firstOrNull {
+            it.id == notificationId
+        }
+    }
+
+    @JvmStatic
+    fun getNotifications(packageName: String): List<StatusBarNotification> {
+        val notifications: MutableList<StatusBarNotification> = ArrayList()
+        for (notification in notificationService.activeNotifications) {
+            if (notification.packageName == packageName) {
+                notifications.add(notification)
+            }
+        }
+        return notifications
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/permission/src/android/permission/cts/NotificationUtils.kt b/tests/tests/permission/src/android/permission/cts/NotificationUtils.kt
new file mode 100644
index 0000000..7c50837
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationUtils.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.service.notification.StatusBarNotification
+import org.junit.Assert
+import java.util.concurrent.TimeUnit
+
+/**
+ * Utility methods to interact with NotificationManager through the CTS NotificationListenerService
+ * to get or clear notifications.
+ */
+object NotificationUtils {
+
+    private val NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5)
+
+    /**
+     * Get a notification listener notification that is currently visible.
+     *
+     * @param cancelNotification if `true` the notification is canceled inside this method
+     * @return The notification or `null` if there is none
+     */
+    @JvmStatic
+    @Throws(Throwable::class)
+    fun getNotificationForPackageAndId(
+        pkg: String,
+        id: Int,
+        cancelNotification: Boolean
+    ): StatusBarNotification? {
+        val notificationService = NotificationListener.getInstance()
+        val notifications: List<StatusBarNotification> = getNotificationsForPackage(pkg)
+        if (notifications.isEmpty()) {
+            return null
+        }
+        for (notification in notifications) {
+            if (notification.id == id) {
+                if (cancelNotification) {
+                    clearNotification(notification)
+                }
+                return notification
+            }
+        }
+        return null
+    }
+
+    /**
+     * Clears all currently visible notifications for a specified package.
+     */
+    @JvmStatic
+    @Throws(Throwable::class)
+    fun clearNotificationsForPackage(pkg: String) {
+        val notifications: List<StatusBarNotification> = getNotificationsForPackage(pkg)
+        if (notifications.isEmpty()) {
+            return
+        }
+
+        clearNotifications(notifications)
+    }
+
+    /** Clears the specified notification and ensures (asserts) it was removed */
+    @JvmStatic
+    @Throws(Throwable::class)
+    fun clearNotification(notification: StatusBarNotification) {
+        val notificationService = NotificationListener.getInstance()
+        notificationService.cancelNotification(notification.key)
+
+        // Wait for notification to get canceled
+        TestUtils.eventually({
+            Assert.assertFalse(
+                listOf(*notificationService.activeNotifications)
+                    .contains(notification)
+            )
+        }, NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS)
+    }
+
+    private fun clearNotifications(notifications: List<StatusBarNotification>) {
+        val notificationService = NotificationListener.getInstance()
+        notifications.forEach { notificationService.cancelNotification(it.key) }
+
+        // Wait for notification to get canceled
+        TestUtils.eventually({
+            val activeNotifications: List<StatusBarNotification> =
+                listOf(*notificationService.activeNotifications)
+            Assert.assertFalse(
+                activeNotifications.any { notifications.contains(it) }
+            )
+        }, NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS)
+    }
+
+    /**
+     * Get all notifications associated with a given package that are currently visible.
+     * @param pkg Package for which to filter the notifications by
+     * @return [List] of [StatusBarNotification]
+     */
+    @Throws(Exception::class)
+    private fun getNotificationsForPackage(pkg: String): List<StatusBarNotification> {
+        val notificationService = NotificationListener.getInstance()
+        val notifications: MutableList<StatusBarNotification> = ArrayList()
+        for (notification in notificationService.activeNotifications) {
+            if (notification.packageName == pkg) {
+                notifications.add(notification)
+            }
+        }
+        return notifications
+    }
+}
\ No newline at end of file
diff --git a/tests/tests/permission/src/android/permission/cts/SafetyCenterUtils.kt b/tests/tests/permission/src/android/permission/cts/SafetyCenterUtils.kt
new file mode 100644
index 0000000..f057b21
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/SafetyCenterUtils.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.permission.cts
+
+import android.app.Instrumentation
+import android.app.UiAutomation
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Build
+import android.os.UserHandle
+import android.provider.DeviceConfig
+import android.safetycenter.SafetyCenterIssue
+import android.safetycenter.SafetyCenterManager
+import android.support.test.uiautomator.By
+import androidx.annotation.RequiresApi
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.compatibility.common.util.UiAutomatorUtils.waitFindObject
+import com.android.safetycenter.internaldata.SafetyCenterIds
+import com.android.safetycenter.internaldata.SafetyCenterIssueId
+import com.android.safetycenter.internaldata.SafetyCenterIssueKey
+import org.junit.Assert
+
+object SafetyCenterUtils {
+    /** Name of the flag that determines whether SafetyCenter is enabled. */
+    const val PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled"
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    /** Returns whether the device supports Safety Center. */
+    @JvmStatic
+    fun deviceSupportsSafetyCenter(context: Context): Boolean {
+        return context.resources.getBoolean(
+            Resources.getSystem().getIdentifier("config_enableSafetyCenter", "bool", "android"))
+    }
+
+    /** Enabled or disable Safety Center */
+    @JvmStatic
+    fun setSafetyCenterEnabled(enabled: Boolean) {
+        setDeviceConfigPrivacyProperty(PROPERTY_SAFETY_CENTER_ENABLED, enabled.toString())
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    @JvmStatic
+    fun startSafetyCenterActivity(context: Context) {
+        context.startActivity(
+            Intent(Intent.ACTION_SAFETY_CENTER)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
+    }
+
+    @JvmStatic
+    fun assertSafetyCenterStarted() {
+        // CollapsingToolbar title can't be found by text, so using description instead.
+        waitFindObject(By.desc("Security & Privacy"))
+    }
+
+    @JvmStatic
+    fun setDeviceConfigPrivacyProperty(
+        propertyName: String,
+        value: String,
+        uiAutomation: UiAutomation = instrumentation.uiAutomation
+    ) {
+        runWithShellPermissionIdentity(uiAutomation) {
+            val valueWasSet =
+                DeviceConfig.setProperty(
+                    DeviceConfig.NAMESPACE_PRIVACY,
+                    /* name = */ propertyName,
+                    /* value = */ value,
+                    /* makeDefault = */ false)
+            check(valueWasSet) { "Could not set $propertyName to $value" }
+        }
+    }
+
+    @JvmStatic
+    private fun getSafetyCenterIssues(
+        automation: UiAutomation = instrumentation.uiAutomation
+    ): List<SafetyCenterIssue> {
+        val safetyCenterManager =
+            instrumentation.targetContext.getSystemService(SafetyCenterManager::class.java)
+        val issues = ArrayList<SafetyCenterIssue>()
+        runWithShellPermissionIdentity(automation) {
+            val safetyCenterData = safetyCenterManager!!.safetyCenterData
+            issues.addAll(safetyCenterData.issues)
+        }
+        return issues
+    }
+
+    @JvmStatic
+    fun assertSafetyCenterIssueExist(
+        sourceId: String,
+        issueId: String,
+        issueTypeId: String,
+        automation: UiAutomation = instrumentation.uiAutomation
+    ) {
+        val safetyCenterIssueId = safetyCenterIssueId(sourceId, issueId, issueTypeId)
+        Assert.assertTrue(
+            "Expect issues in safety center",
+            getSafetyCenterIssues(automation).any { safetyCenterIssueId == it.id })
+    }
+
+    @JvmStatic
+    fun assertSafetyCenterIssueDoesNotExist(
+        sourceId: String,
+        issueId: String,
+        issueTypeId: String,
+        automation: UiAutomation = instrumentation.uiAutomation
+    ) {
+        val safetyCenterIssueId = safetyCenterIssueId(sourceId, issueId, issueTypeId)
+        Assert.assertTrue(
+            "Expect no issue in safety center",
+            getSafetyCenterIssues(automation).none { safetyCenterIssueId == it.id })
+    }
+
+    private fun safetyCenterIssueId(sourceId: String, sourceIssueId: String, issueTypeId: String) =
+        SafetyCenterIds.encodeToString(
+            SafetyCenterIssueId.newBuilder()
+                .setSafetyCenterIssueKey(
+                    SafetyCenterIssueKey.newBuilder()
+                        .setSafetySourceId(sourceId)
+                        .setSafetySourceIssueId(sourceIssueId)
+                        .setUserId(UserHandle.myUserId())
+                        .build())
+                .setIssueTypeId(issueTypeId)
+                .build())
+}
diff --git a/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt b/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
index fe9037a..90e613d 100644
--- a/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
+++ b/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
@@ -26,13 +26,20 @@
 import android.hardware.camera2.CameraManager
 import android.os.Build
 import android.os.Process
+import android.os.SystemClock
 import android.permission.PermissionManager
 import android.provider.DeviceConfig
 import android.provider.Settings
+import android.safetycenter.SafetyCenterManager
 import android.server.wm.WindowManagerStateHelper
 import android.support.test.uiautomator.By
+import android.support.test.uiautomator.BySelector
+import android.support.test.uiautomator.StaleObjectException
 import android.support.test.uiautomator.UiDevice
+import android.support.test.uiautomator.UiObject2
+import android.support.test.uiautomator.UiScrollable
 import android.support.test.uiautomator.UiSelector
+import androidx.annotation.RequiresApi
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.compatibility.common.util.DisableAnimationRule
@@ -40,6 +47,7 @@
 import com.android.compatibility.common.util.SystemUtil.eventually
 import com.android.compatibility.common.util.SystemUtil.runShellCommand
 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.compatibility.common.util.UiAutomatorUtils
 import org.junit.After
 import org.junit.Assert
 import org.junit.Assert.assertEquals
@@ -50,7 +58,6 @@
 import org.junit.Assume.assumeFalse
 import org.junit.Assume.assumeTrue
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -102,7 +109,7 @@
 
     private val safetyCenterEnabled = callWithShellPermissionIdentity {
         DeviceConfig.getString(DeviceConfig.NAMESPACE_PRIVACY,
-                SAFETY_CENTER_ENABLED, false.toString())
+            SAFETY_CENTER_ENABLED, false.toString())
     }
 
     @Before
@@ -115,7 +122,7 @@
                 context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, 1800000L
             )
             DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
-                    SAFETY_CENTER_ENABLED, false.toString(), false)
+                SAFETY_CENTER_ENABLED, false.toString(), false)
         }
 
         if (!isScreenOn) {
@@ -207,9 +214,7 @@
         testCameraAndMicIndicator(useMic = false, useCamera = true, chainUsage = true)
     }
 
-    // Enable when safety center sends a broadcast on safety center flag value change
     @Test
-    @Ignore
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
     fun testSafetyCenterCameraIndicator() {
         assumeFalse(isTv)
@@ -217,28 +222,27 @@
         val manager = context.getSystemService(CameraManager::class.java)!!
         assumeTrue(manager.cameraIdList.isNotEmpty())
         changeSafetyCenterFlag(true.toString())
+        assumeSafetyCenterEnabled()
         testCameraAndMicIndicator(useMic = false, useCamera = true, safetyCenterEnabled = true)
     }
 
-    // Enable when safety center sends a broadcast on safety center flag value change
     @Test
-    @Ignore
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
     fun testSafetyCenterMicIndicator() {
         assumeFalse(isTv)
         assumeFalse(isCar)
         changeSafetyCenterFlag(true.toString())
+        assumeSafetyCenterEnabled()
         testCameraAndMicIndicator(useMic = true, useCamera = false, safetyCenterEnabled = true)
     }
 
-    // Enable when safety center sends a broadcast on safety center flag value change
     @Test
-    @Ignore
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
     fun testSafetyCenterHotwordIndicatorBehavior() {
         assumeFalse(isTv)
         assumeFalse(isCar)
         changeSafetyCenterFlag(true.toString())
+        assumeSafetyCenterEnabled()
         testCameraAndMicIndicator(
             useMic = false,
             useCamera = false,
@@ -247,14 +251,13 @@
         )
     }
 
-    // Enable when safety center sends a broadcast on safety center flag value change
     @Test
-    @Ignore
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
     fun testSafetyCenterChainUsageWithOtherUsage() {
         assumeFalse(isTv)
         assumeFalse(isCar)
         changeSafetyCenterFlag(true.toString())
+        assumeSafetyCenterEnabled()
         testCameraAndMicIndicator(
             useMic = false,
             useCamera = true,
@@ -295,7 +298,7 @@
                 // indicator
                 uiDevice.openQuickSettings()
                 assertPrivacyChipAndIndicatorsPresent(
-                    useMic,
+                    useMic || useHotword,
                     useCamera,
                     chainUsage,
                     safetyCenterEnabled
@@ -390,13 +393,11 @@
         chainUsage: Boolean,
         safetyCenterEnabled: Boolean = false
     ) {
-        // Ensure the privacy chip is present (or not)
-        val chipFound = isChipPresent()
-        if (useMic || useCamera) {
-            assertTrue("Did not find chip", chipFound)
-        } else { // hotword
-            assertFalse("Found chip, but did not expect to", chipFound)
-            return
+        // Ensure the privacy chip is present
+        eventually {
+            val privacyChip = uiDevice.findObject(UiSelector().resourceId(PRIVACY_CHIP_ID))
+            assertTrue("view with id $PRIVACY_CHIP_ID not found", privacyChip.exists())
+            privacyChip.click()
         }
 
         eventually {
@@ -405,15 +406,23 @@
                 return@eventually
             }
             if (useMic) {
-                val iconView = uiDevice.findObject(UiSelector().descriptionContains(micLabel))
-                assertTrue("View with description $micLabel not found", iconView.exists())
+                var iconView = if (safetyCenterEnabled) {
+                    waitFindObjectOrNull(By.text(micLabel))
+                } else {
+                    uiDevice.findObject(UiSelector().descriptionContains(micLabel))
+                }
+                assertNotNull("View with description $micLabel not found", iconView)
             }
             if (useCamera) {
-                val iconView = uiDevice.findObject(UiSelector().descriptionContains(cameraLabel))
-                assertTrue("View with text $APP_LABEL not found", iconView.exists())
+                var iconView = if (safetyCenterEnabled) {
+                    waitFindObjectOrNull(By.text(cameraLabel))
+                } else {
+                    uiDevice.findObject(UiSelector().descriptionContains(cameraLabel))
+                }
+                assertNotNull("View with text $APP_LABEL not found", iconView)
             }
-            val appView = uiDevice.findObject(UiSelector().textContains(APP_LABEL))
-            assertTrue("View with text $APP_LABEL not found", appView.exists())
+            var appView = waitFindObjectOrNull(By.textContains(APP_LABEL))
+            assertNotNull("View with text $APP_LABEL not found", appView)
             if (safetyCenterEnabled) {
                 assertTrue("Did not find safety center views",
                     uiDevice.findObjects(By.res(SAFETY_CENTER_ITEM_ID)).size > 0)
@@ -445,31 +454,23 @@
             "Did not find shell package"
         }
 
-        val usageViews = if (safetyCenterEnabled) {
-            uiDevice.findObjects(By.res(SAFETY_CENTER_ITEM_ID))
+        if (safetyCenterEnabled) {
+            val appView = UiScrollable(UiSelector().scrollable(true))
+            appView.scrollIntoView(UiSelector().resourceId(SAFETY_CENTER_ITEM_ID))
+            var micView = waitFindObjectOrNull(By.text(micLabel))
+            assertNotNull("View with text $micLabel not found", micView)
+            var camView = waitFindObjectOrNull(By.text(cameraLabel))
+            assertNotNull("View with text $cameraLabel not found", camView)
+            var shellView = waitFindObjectOrNull(By.textContains(shellLabel))
+            assertNotNull("View with text $shellLabel not found", shellView)
         } else {
-            uiDevice.findObjects(By.res(PRIVACY_ITEM_ID))
+            val usageViews = uiDevice.findObjects(By.res(PRIVACY_ITEM_ID))
+            assertEquals("Expected two usage views", 2, usageViews.size)
+            val appViews = uiDevice.findObjects(By.textContains(APP_LABEL))
+            assertEquals("Expected two $APP_LABEL view", 2, appViews.size)
+            val shellView = uiDevice.findObjects(By.textContains(shellLabel))
+            assertEquals("Expected only one shell view", 1, shellView.size)
         }
-        assertEquals("Expected two usage views", 2, usageViews.size)
-        val appViews = uiDevice.findObjects(By.textContains(APP_LABEL))
-        assertEquals("Expected two $APP_LABEL view", 2, appViews.size)
-        val shellView = uiDevice.findObjects(By.textContains(shellLabel))
-        assertEquals("Expected only one shell view", 1, shellView.size)
-    }
-
-    private fun isChipPresent(): Boolean {
-        var chipFound = false
-        try {
-            eventually {
-                val privacyChip = uiDevice.findObject(By.res(PRIVACY_CHIP_ID))
-                assertNotNull("view with id $PRIVACY_CHIP_ID not found", privacyChip)
-                privacyChip.click()
-                chipFound = true
-            }
-        } catch (e: Exception) {
-            // Handle more gracefully after
-        }
-        return chipFound
     }
 
     private fun pressBack() {
@@ -488,7 +489,38 @@
     private fun changeSafetyCenterFlag(safetyCenterEnabled: String) {
         runWithShellPermissionIdentity {
             DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
-                    SAFETY_CENTER_ENABLED, safetyCenterEnabled, false)
+                SAFETY_CENTER_ENABLED, safetyCenterEnabled, false)
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    private fun assumeSafetyCenterEnabled() {
+        val safetyCenterManager = context.getSystemService(SafetyCenterManager::class.java)!!
+        val isSafetyCenterEnabled: Boolean = runWithShellPermissionIdentity<Boolean> {
+            safetyCenterManager.isSafetyCenterEnabled
+        }
+        assumeTrue(isSafetyCenterEnabled)
+    }
+
+    protected fun waitFindObjectOrNull(selector: BySelector): UiObject2? {
+        waitForIdle()
+        return findObjectWithRetry({ t -> UiAutomatorUtils.waitFindObjectOrNull(selector, t) })
+    }
+
+    private fun findObjectWithRetry(
+        automatorMethod: (timeoutMillis: Long) -> UiObject2?,
+        timeoutMillis: Long = TIMEOUT_MILLIS
+    ): UiObject2? {
+        waitForIdle()
+        val startTime = SystemClock.elapsedRealtime()
+        return try {
+            automatorMethod(timeoutMillis)
+        } catch (e: StaleObjectException) {
+            val remainingTime = timeoutMillis - (SystemClock.elapsedRealtime() - startTime)
+            if (remainingTime <= 0) {
+                throw e
+            }
+            automatorMethod(remainingTime)
         }
     }
 }
\ No newline at end of file
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/TestHelper.java b/tests/tests/wifi/src/android/net/wifi/cts/TestHelper.java
index 02193c3..7f679ff 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/TestHelper.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/TestHelper.java
@@ -786,6 +786,7 @@
                     // be the primary connection.
                     if (mWifiManager.isStaConcurrencyForLocalOnlyConnectionsSupported()) {
                         assertThat(wifiInfo.isPrimary()).isFalse();
+                        assertConnectionEquals(network, mWifiManager.getConnectionInfo());
                     } else {
                         assertThat(wifiInfo.isPrimary()).isTrue();
                     }
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
index 3b004dd..a054aa5 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
@@ -1880,6 +1880,7 @@
         TestExecutor executor = new TestExecutor();
         TestSoftApCallback lohsSoftApCallback = new TestSoftApCallback(mLock);
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        setWifiEnabled(false);
         boolean wifiEnabled = mWifiManager.isWifiEnabled();
         try {
             uiAutomation.adoptShellPermissionIdentity();