Merge "CTS test for Android Security b/179042963" into tm-dev am: 6f4b53d77a

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

Change-Id: Idc0ddf25ef002e36ce2db69825471a327a30d7db
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 f85e62b..ba1ae3b 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -3051,24 +3051,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"
@@ -5778,20 +5760,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 2a29e98..c8248b1 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -6678,41 +6678,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 c22ac4f..39035ca 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
@@ -19,7 +19,6 @@
 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.IntentFilter;
@@ -27,7 +26,6 @@
 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.os.BatteryManager;
 import android.telephony.TelephonyManager;
@@ -148,10 +146,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";
@@ -495,10 +489,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;
                 }
@@ -605,16 +595,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 5c3ee1c..793b674 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();
 
@@ -437,39 +494,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();
     }
@@ -481,7 +527,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..");
@@ -498,7 +544,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 (!isReportLogOkToPass()) {
                 mResultTest.setText(getResources().getString(R.string.audio_general_reportlogtest));
@@ -511,7 +557,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/AudioDescriptorActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioDescriptorActivity.java
index 8293816..52164a5 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioDescriptorActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioDescriptorActivity.java
@@ -134,11 +134,6 @@
     }
 
     @Override
-    public boolean requiresReportLog() {
-        return true;
-    }
-
-    @Override
     public void recordTestResults() {
         CtsVerifierReportLog reportLog = getReportLog();
 
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 2b95258..724f498 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 dcde897..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/audio/AudioMicrophoneMuteToggleActivity.java
+++ /dev/null
@@ -1,218 +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";
-
-    public AudioMicrophoneMuteToggleActivity() {
-    }
-
-    @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 boolean requiresReportLog() {
-        return true;
-    }
-
-    @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 232e14d..cac4659 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;
@@ -184,12 +180,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());
@@ -266,17 +261,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(
@@ -291,8 +286,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]);
       }
 
@@ -307,8 +302,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]);
       }
 
@@ -322,7 +317,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");
@@ -338,7 +333,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);
   }
 
   /**
@@ -368,7 +363,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 a5a11f2..5f1be38 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;
@@ -164,11 +160,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());
@@ -224,18 +220,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(
@@ -250,8 +246,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]);
       }
 
@@ -266,8 +262,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]);
       }
 
@@ -281,7 +277,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");
@@ -297,7 +293,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);
   }
 
   /**
@@ -327,7 +323,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/apps/PermissionApp/Android.bp b/apps/PermissionApp/Android.bp
index 70c7d67..df62a77 100644
--- a/apps/PermissionApp/Android.bp
+++ b/apps/PermissionApp/Android.bp
@@ -30,6 +30,6 @@
         "arcts",
         "cts",
         "general-tests",
-        "mts",
+        "mts-permission",
     ],
 }
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/Android.bp b/common/device-side/util-axt/Android.bp
index 11f851b..187409b 100644
--- a/common/device-side/util-axt/Android.bp
+++ b/common/device-side/util-axt/Android.bp
@@ -16,8 +16,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-java_library_static {
-    name: "compatibility-device-util-axt",
+java_defaults {
+    name: "compatibility-device-util-axt-default",
     sdk_version: "test_current",
 
     srcs: [
@@ -31,8 +31,8 @@
         "androidx.test.rules",
         "androidx.test.ext.junit",
         "ub-uiautomator",
-        "mockito-target-minus-junit4",
         "androidx.annotation_annotation",
+        "androidx.test.uiautomator_uiautomator",
         "truth-prebuilt",
         "modules-utils-build_system",
     ],
@@ -45,9 +45,26 @@
     jarjar_rules: "protobuf-jarjar-rules.txt",
 }
 
+java_library_static {
+    name: "compatibility-device-util-axt",
+    defaults: ["compatibility-device-util-axt-default"],
+    static_libs: [
+        "mockito-target-minus-junit4",
+    ],
+}
+
+// This target can be used with ExtendedMockito
+java_library {
+    name: "compatibility-device-util-axt-minus-dexmaker",
+    defaults: ["compatibility-device-util-axt-default"],
+    static_libs: [
+        "mockito",
+    ],
+}
+
 filegroup {
     name: "compatibility-device-util-nodeps",
     srcs: [
-         "src/com/android/compatibility/common/util/IBinderParcelable.java",
+        "src/com/android/compatibility/common/util/IBinderParcelable.java",
     ],
 }
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/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaUtils.java
index c4bb536..e8ed8c7 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaUtils.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaUtils.java
@@ -1548,6 +1548,27 @@
         return false;
     }
 
+    /**
+     *  Function to identify if the device is a cuttlefish instance
+     */
+    public static boolean onCuttlefish() throws IOException {
+        String device = SystemProperties.get("ro.product.device", "");
+        String model = SystemProperties.get("ro.product.model", "");
+        String name = SystemProperties.get("ro.product.name", "");
+
+        // Return true for cuttlefish instances
+        if (!device.startsWith("vsoc_")) {
+            return false;
+        }
+        if (!model.startsWith("Cuttlefish ")) {
+            return false;
+        }
+        if (name.startsWith("cf_") || name.startsWith("aosp_cf_")) {
+            return true;
+        }
+        return false;
+    }
+
     /*
      *  -------------------------------------- END --------------------------------------
      */
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java
index 0b70c8b..e306c12 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java
@@ -29,15 +29,13 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.modules.utils.build.SdkLevel;
 
-import java.io.ByteArrayOutputStream;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.util.concurrent.Callable;
-import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Predicate;
 
@@ -400,4 +398,18 @@
             }
         }
     }
+
+    /**
+     * Use `wait-for-broadcast-barrier` on U+
+     * and `wait-for-broadcast-idle` on previous version
+     */
+    public static void waitForBroadcasts() {
+        String cmd;
+        if (SdkLevel.isAtLeastU()) {
+            cmd = "am wait-for-broadcast-barrier";
+        } else {
+            cmd = "am wait-for-broadcast-idle";
+        }
+        runShellCommand(cmd);
+    }
 }
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils.java
index 1611e13..548c5a4 100644
--- a/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils.java
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils.java
@@ -36,6 +36,9 @@
 
 import java.util.regex.Pattern;
 
+/**
+ * @deprecated , Use {@link UiAutomatorUtils2}, which uses latest androidx automator classes.
+ */
 public class UiAutomatorUtils {
     private UiAutomatorUtils() {}
 
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils2.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils2.java
new file mode 100644
index 0000000..b677dbf
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/UiAutomatorUtils2.java
@@ -0,0 +1,167 @@
+package com.android.compatibility.common.util;
+/*
+ * 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.
+ */
+
+
+import static org.junit.Assert.assertNotNull;
+
+import android.graphics.Rect;
+import android.util.Log;
+import android.util.TypedValue;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.StaleObjectException;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.UiObjectNotFoundException;
+import androidx.test.uiautomator.UiScrollable;
+import androidx.test.uiautomator.UiSelector;
+import androidx.test.uiautomator.Until;
+
+import java.util.regex.Pattern;
+
+public class UiAutomatorUtils2 {
+    private UiAutomatorUtils2() {}
+
+    private static final String LOG_TAG = "UiAutomatorUtils";
+
+    /** Default swipe deadzone percentage. See {@link UiScrollable}. */
+    private static final double DEFAULT_SWIPE_DEADZONE_PCT = 0.1;
+
+    /** Minimum view height accepted (before needing to scroll more). */
+    private static final float MIN_VIEW_HEIGHT_DP = 8;
+
+    private static Pattern sCollapsingToolbarResPattern =
+            Pattern.compile(".*:id/collapsing_toolbar");
+
+    public static UiDevice getUiDevice() {
+        return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    public static UiObject2 waitFindObject(BySelector selector) throws UiObjectNotFoundException {
+        return waitFindObject(selector, 20_000);
+    }
+
+    public static UiObject2 waitFindObject(BySelector selector, long timeoutMs)
+            throws UiObjectNotFoundException {
+        final UiObject2 view = waitFindObjectOrNull(selector, timeoutMs);
+        ExceptionUtils.wrappingExceptions(UiDumpUtils::wrapWithUiDump, () -> {
+            assertNotNull("View not found after waiting for " + timeoutMs + "ms: " + selector,
+                    view);
+        });
+        return view;
+    }
+
+    public static UiObject2 waitFindObjectOrNull(BySelector selector)
+            throws UiObjectNotFoundException {
+        return waitFindObjectOrNull(selector, 20_000);
+    }
+
+    private static int convertDpToPx(float dp) {
+        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
+                ApplicationProvider.getApplicationContext().getResources().getDisplayMetrics()));
+    }
+
+    public static UiObject2 waitFindObjectOrNull(BySelector selector, long timeoutMs)
+            throws UiObjectNotFoundException {
+        UiObject2 view = null;
+        long start = System.currentTimeMillis();
+
+        boolean isAtEnd = false;
+        boolean wasScrolledUpAlready = false;
+        boolean scrolledPastCollapsibleToolbar = false;
+
+        final int minViewHeightPx = convertDpToPx(MIN_VIEW_HEIGHT_DP);
+
+        while (view == null && start + timeoutMs > System.currentTimeMillis()) {
+            try {
+                view = getUiDevice().wait(Until.findObject(selector), 1000);
+            } catch (StaleObjectException exception) {
+                // UiDevice.wait() may cause StaleObjectException if the {@link View} attached to
+                // UiObject2 is no longer in the view tree.
+                Log.v(LOG_TAG, "UiObject2 view is no longer in the view tree.", exception);
+                getUiDevice().waitForIdle();
+                continue;
+            }
+
+            if (view == null || view.getVisibleBounds().height() < minViewHeightPx) {
+                final double deadZone = !(FeatureUtil.isWatch() || FeatureUtil.isTV())
+                        ? 0.25 : DEFAULT_SWIPE_DEADZONE_PCT;
+                UiScrollable scrollable = new UiScrollable(new UiSelector().scrollable(true));
+                scrollable.setSwipeDeadZonePercentage(deadZone);
+                if (scrollable.exists()) {
+                    if (!scrolledPastCollapsibleToolbar) {
+                        scrollPastCollapsibleToolbar(scrollable, deadZone);
+                        scrolledPastCollapsibleToolbar = true;
+                        continue;
+                    }
+                    if (isAtEnd) {
+                        if (wasScrolledUpAlready) {
+                            return null;
+                        }
+                        scrollable.scrollToBeginning(Integer.MAX_VALUE);
+                        isAtEnd = false;
+                        wasScrolledUpAlready = true;
+                        scrolledPastCollapsibleToolbar = false;
+                    } else {
+                        Rect boundsBeforeScroll = scrollable.getBounds();
+                        boolean scrollAtStartOrEnd = !scrollable.scrollForward();
+                        // The scrollable view may no longer be scrollable after the toolbar is
+                        // collapsed.
+                        if (scrollable.exists()) {
+                            Rect boundsAfterScroll = scrollable.getBounds();
+                            isAtEnd = scrollAtStartOrEnd && boundsBeforeScroll.equals(
+                                    boundsAfterScroll);
+                        } else {
+                            isAtEnd = scrollAtStartOrEnd;
+                        }
+                    }
+                } else {
+                    // There might be a collapsing toolbar, but no scrollable view. Try to collapse
+                    scrollPastCollapsibleToolbar(null, deadZone);
+                }
+            }
+        }
+        return view;
+    }
+
+    private static void scrollPastCollapsibleToolbar(UiScrollable scrollable, double deadZone)
+            throws UiObjectNotFoundException {
+        final UiObject2 collapsingToolbar = getUiDevice().findObject(
+                By.res(sCollapsingToolbarResPattern));
+        if (collapsingToolbar == null) {
+            return;
+        }
+
+        final int steps = 55; // == UiScrollable.SCROLL_STEPS
+        if (scrollable != null && scrollable.exists()) {
+            final Rect scrollableBounds = scrollable.getVisibleBounds();
+            final int distanceToSwipe = collapsingToolbar.getVisibleBounds().height() / 2;
+            getUiDevice().drag(scrollableBounds.centerX(), scrollableBounds.centerY(),
+                    scrollableBounds.centerX(), scrollableBounds.centerY() - distanceToSwipe,
+                    steps);
+        } else {
+            // There might be a collapsing toolbar, but no scrollable view. Try to collapse
+            int maxY = getUiDevice().getDisplayHeight();
+            int minY = (int) (deadZone * maxY);
+            maxY -= minY;
+            getUiDevice().drag(0, maxY, 0, minY, steps);
+        }
+    }
+}
diff --git a/hostsidetests/appcompat/strictjavapackages/Android.bp b/hostsidetests/appcompat/strictjavapackages/Android.bp
index 272be12..2656bbc 100644
--- a/hostsidetests/appcompat/strictjavapackages/Android.bp
+++ b/hostsidetests/appcompat/strictjavapackages/Android.bp
@@ -27,6 +27,7 @@
     ],
     static_libs: [
         "compat-classpaths-testing",
+        "modules-utils-build-testing",
         "dexlib2-no-guava-no-cli",
     ],
     // tag this module as a cts test artifact
diff --git a/hostsidetests/appcompat/strictjavapackages/app/AndroidManifest.xml b/hostsidetests/appcompat/strictjavapackages/app/AndroidManifest.xml
index e610581..f76eab4 100644
--- a/hostsidetests/appcompat/strictjavapackages/app/AndroidManifest.xml
+++ b/hostsidetests/appcompat/strictjavapackages/app/AndroidManifest.xml
@@ -20,6 +20,7 @@
           android:targetSandboxVersion="2">
 
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
     <application android:requestLegacyExternalStorage="true">
         <uses-library android:name="android.test.runner" />
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 d02dc42..551f1774 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,16 @@
 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.Optional;
 import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -74,12 +82,16 @@
     private static final String TEST_HELPER_PACKAGE = "android.compat.sjp.app";
     private static final String TEST_HELPER_APK = "StrictJavaPackagesTestApp.apk";
 
+    private static final Pattern APEX_JAR_PATTERN =
+            Pattern.compile("\\/apex\\/(?<apexName>[^\\/]+)\\/.*\\.(jar|apk)");
+
     private static ImmutableList<String> sBootclasspathJars;
     private static ImmutableList<String> sSystemserverclasspathJars;
     private static ImmutableList<String> sSharedLibJars;
     private static ImmutableList<SharedLibraryInfo> sSharedLibs;
     private static ImmutableMultimap<String, String> sSharedLibsPathsToName;
     private static ImmutableMultimap<String, String> sJarsToClasses;
+    private static ImmutableMultimap<String, String> sJarsToFiles;
 
     private DeviceSdkLevel mDeviceSdkLevel;
 
@@ -201,6 +213,7 @@
                     "Landroid/os/IVoldListener;",
                     "Landroid/os/IVoldMountCallback;",
                     "Landroid/os/IVoldTaskListener;",
+                    "Landroid/os/TouchOcclusionMode;",
                     "Landroid/os/storage/CrateMetadata;",
                     "Landroid/view/LayerMetadataKey;",
                     "Lcom/android/internal/annotations/CompositeRWLock;",
@@ -737,7 +750,8 @@
                 "Lcom/android/sdksandbox/ISdkSandboxService;",
                 "Lcom/android/sdksandbox/SandboxLatencyInfo-IA;",
                 "Lcom/android/sdksandbox/SandboxLatencyInfo;",
-                "Lcom/android/sdksandbox/IUnloadSdkCallback;"
+                "Lcom/android/sdksandbox/IUnloadSdkCallback;",
+                "Lcom/android/sdksandbox/IComputeSdkStorageCallback;"
             );
 
     private static final ImmutableMap<String, ImmutableSet<String>> FULL_APK_IN_APEX_BURNDOWN =
@@ -817,6 +831,8 @@
         });
         sSharedLibsPathsToName = sharedLibsPathsToName.build();
 
+        final ImmutableSetMultimap.Builder<String, String> jarsToFiles =
+                ImmutableSetMultimap.builder();
         final ImmutableSetMultimap.Builder<String, String> jarsToClasses =
                 ImmutableSetMultimap.builder();
         Stream.of(sBootclasspathJars.stream(),
@@ -824,21 +840,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();
     }
 
@@ -852,7 +879,7 @@
      */
     @Test
     public void testBootclasspath_nonDuplicateClasses() throws Exception {
-        assumeTrue(mDeviceSdkLevel.isDeviceAtLeastR());
+        assumeTrue(mDeviceSdkLevel.isDeviceAtLeastT());
         assertThat(getDuplicateClasses(sBootclasspathJars)).isEmpty();
     }
 
@@ -861,7 +888,7 @@
      */
     @Test
     public void testSystemServerClasspath_nonDuplicateClasses() throws Exception {
-        assumeTrue(mDeviceSdkLevel.isDeviceAtLeastR());
+        assumeTrue(mDeviceSdkLevel.isDeviceAtLeastT());
         ImmutableSet<String> overlapBurndownList;
         if (hasFeature(FEATURE_AUTOMOTIVE)) {
             overlapBurndownList = ImmutableSet.copyOf(AUTOMOTIVE_HIDL_OVERLAP_BURNDOWN_LIST);
@@ -883,7 +910,7 @@
      */
     @Test
     public void testBootClasspathAndSystemServerClasspath_nonDuplicateClasses() throws Exception {
-        assumeTrue(mDeviceSdkLevel.isDeviceAtLeastR());
+        assumeTrue(mDeviceSdkLevel.isDeviceAtLeastT());
         ImmutableList.Builder<String> jars = ImmutableList.builder();
         jars.addAll(sBootclasspathJars);
         jars.addAll(sSystemserverclasspathJars);
@@ -901,7 +928,8 @@
         }
         Multimap<String, String> duplicates = getDuplicateClasses(jars.build());
         Multimap<String, String> filtered = Multimaps.filterKeys(duplicates,
-                duplicate -> !overlapBurndownList.contains(duplicate));
+                duplicate -> !overlapBurndownList.contains(duplicate)
+                        && !jarsInSameApex(duplicates.get(duplicate)));
 
         assertThat(filtered).isEmpty();
     }
@@ -1000,11 +1028,14 @@
         HashMultimap<String, Multimap<String, String>> perApkClasspathDuplicates =
                 HashMultimap.create();
         Arrays.stream(collectApkInApexPaths())
+                .filter(apk -> apk != null && !apk.isEmpty())
                 .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,
@@ -1031,6 +1062,8 @@
                         }
                     } catch (Exception e) {
                         throw new RuntimeException(e);
+                    } finally {
+                        FileUtil.deleteFile(apkFile);
                     }
                 });
         assertThat(perApkClasspathDuplicates).isEmpty();
@@ -1041,20 +1074,25 @@
      * and shared library jars.
      */
     @Test
-    public void testBootClasspathAndSystemServerClasspathAndSharedLibs_noAndroidxDependencies() {
+    public void testBootClasspathAndSystemServerClasspathAndSharedLibs_noAndroidxDependencies()
+            throws Exception {
+        assumeTrue(mDeviceSdkLevel.isDeviceAtLeastT());
         // WARNING: Do not add more exceptions here, no androidx should be in bootclasspath.
         // See go/androidx-api-guidelines#module-naming for more details.
         final ImmutableMap<String, ImmutableSet<String>>
                 LegacyExemptAndroidxSharedLibsNamesToClasses =
                 new ImmutableMap.Builder<String, ImmutableSet<String>>()
                 .put("androidx.camera.extensions.impl",
-                    ImmutableSet.of("Landroidx/camera/extensions/impl/"))
+                    ImmutableSet.of("Landroidx/camera/extensions/impl/",
+                    "Landroidx/camera/extensions/impl/advanced/", "Landroidx/annotation"))
                 .put("androidx.window.extensions",
                     ImmutableSet.of("Landroidx/window/common/", "Landroidx/window/extensions/",
                         "Landroidx/window/util/"))
                 .put("androidx.window.sidecar",
                     ImmutableSet.of("Landroidx/window/common/", "Landroidx/window/sidecar",
                         "Landroidx/window/util"))
+                .put("com.google.android.camera.experimental2019",
+                    ImmutableSet.of("Landroidx/annotation"))
                 .put("com.google.android.camera.experimental2020_midyear",
                     ImmutableSet.of("Landroidx/annotation"))
                 .build();
@@ -1070,6 +1108,73 @@
                 ).isEmpty();
     }
 
+    /**
+     * Ensure that there are no kotlin files in BOOTCLASSPATH, SYSTEMSERVERCLASSPATH
+     * and shared library jars.
+     */
+    @Test
+    public void testNoKotlinFilesInClasspaths() throws Exception {
+        // This test was not in CTS until U.
+        assumeTrue(mDeviceSdkLevel.isDeviceAtLeastU());
+        ImmutableList<String> kotlinFiles =
+                Stream.of(sBootclasspathJars.stream(),
+                        sSystemserverclasspathJars.stream(),
+                        sSharedLibJars.stream())
+                .reduce(Stream::concat).orElseGet(Stream::empty)
+                .parallel()
+                .filter(jarPath -> {
+                    // Exclude shared library apks.
+                    return jarPath.endsWith(".jar")
+                            && sJarsToFiles.get(jarPath)
+                                .stream()
+                                .anyMatch(file -> file.contains(".kotlin_builtins")
+                                        || file.contains(".kotlin_module"));
+                })
+                .collect(ImmutableList.toImmutableList());
+        assertThat(kotlinFiles).isEmpty();
+    }
+
+    /**
+     * Ensure that all classes from protobuf libraries are jarjared before
+     * included in BOOTCLASSPATH, SYSTEMSERVERCLASSPATH and shared library jars
+     */
+    @Test
+    public void testNoProtobufClassesWithoutJarjar() throws Exception {
+        assumeTrue(mDeviceSdkLevel.isDeviceAtLeastU());
+        assertWithMessage("Classes from protobuf libraries must not be included in bootclasspath "
+            + "and systemserverclasspath without being jarjared.")
+                .that(Stream.of(sBootclasspathJars.stream(),
+                                sSystemserverclasspathJars.stream(),
+                                sSharedLibJars.stream())
+                        .reduce(Stream::concat).orElseGet(Stream::empty)
+                        .parallel()
+                        .filter(jarPath -> {
+                            return sJarsToClasses
+                                    .get(jarPath)
+                                    .stream()
+                                    .anyMatch(cls -> cls.startsWith("Lcom/google/protobuf/"));
+                        })
+                        .collect(ImmutableList.toImmutableList())
+                ).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>> legacyExemptAndroidxSharedLibsNamesToClasses,
             String path, String className) {
@@ -1121,6 +1226,21 @@
         return Multimaps.filterKeys(allClasses, key -> allClasses.get(key).size() > 1);
     }
 
+    private boolean jarsInSameApex(Collection<String> jars) {
+        return jars.stream()
+            .map(path -> apexForJar(path).orElse(path))
+            .distinct()
+            .count() <= 1;
+    }
+
+    private Optional<String> apexForJar(String jar) {
+        Matcher m = APEX_JAR_PATTERN.matcher(jar);
+        if (!m.matches()) {
+            return Optional.empty();
+        }
+        return Optional.of(m.group("apexName"));
+    }
+
     private static boolean doesFileExist(String path, ITestDevice device) {
         assertThat(path).isNotNull();
         try {
diff --git a/hostsidetests/appsecurity/OWNERS b/hostsidetests/appsecurity/OWNERS
index 1164206..fdb7a47 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/appsecurity/test-apps/DocumentClient/Android.bp b/hostsidetests/appsecurity/test-apps/DocumentClient/Android.bp
index ed22bab..d6b736d 100644
--- a/hostsidetests/appsecurity/test-apps/DocumentClient/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/DocumentClient/Android.bp
@@ -38,7 +38,9 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts",
+        "mts-documentsui",
+        "mts-mainline-infra",
+        "mts-mediaprovider",
         "sts",
     ],
     certificate: ":cts-testkey2",
diff --git a/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.bp b/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.bp
index b42c9ac..723d063 100644
--- a/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.bp
+++ b/hostsidetests/appsecurity/test-apps/DocumentProvider/Android.bp
@@ -33,7 +33,9 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts",
+        "mts-documentsui",
+        "mts-mainline-infra",
+        "mts-mediaprovider",
         "sts",
     ],
     certificate: ":cts-testkey1",
diff --git a/hostsidetests/appsecurity/test-apps/EncryptionApp/src/com/android/cts/encryptionapp/EncryptionAppTest.java b/hostsidetests/appsecurity/test-apps/EncryptionApp/src/com/android/cts/encryptionapp/EncryptionAppTest.java
index c411ea3..1312ccb 100644
--- a/hostsidetests/appsecurity/test-apps/EncryptionApp/src/com/android/cts/encryptionapp/EncryptionAppTest.java
+++ b/hostsidetests/appsecurity/test-apps/EncryptionApp/src/com/android/cts/encryptionapp/EncryptionAppTest.java
@@ -348,6 +348,7 @@
         assertQuery(2, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE);
 
         if (Environment.isExternalStorageEmulated()) {
+            pollForExternalStorageMountedState();
             assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
 
             final File expected = new File(
@@ -371,6 +372,15 @@
                 ceFile::exists);
     }
 
+    private void pollForExternalStorageMountedState() {
+        for (int i = 0; i < 10; i++) {
+            if (Environment.getExternalStorageState().equalsIgnoreCase(Environment.MEDIA_MOUNTED)) {
+                break;
+            }
+            SystemClock.sleep(500);
+        }
+    }
+
     private void assertQuery(int count, int flags) throws Exception {
         final Intent intent = new Intent(TEST_ACTION);
         assertEquals("activity", count, mPm.queryIntentActivities(intent, flags).size());
diff --git a/hostsidetests/multidevices/uwb/Android.bp b/hostsidetests/multidevices/uwb/Android.bp
deleted file mode 100644
index cee671a..0000000
--- a/hostsidetests/multidevices/uwb/Android.bp
+++ /dev/null
@@ -1,34 +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 {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-python_test_host {
-    name: "CtsUwbMultiDeviceTestCases",
-    main: "uwb_manager_test.py",
-    srcs: ["uwb_manager_test.py"],
-    test_suites: [
-        "cts",
-        "general-tests",
-    ],
-    test_options: {
-        unit_test: false,
-    },
-    data: [
-        // Package the snippet with the mobly test
-        ":uwb_snippet",
-    ],
-}
diff --git a/hostsidetests/multidevices/uwb/AndroidTest.xml b/hostsidetests/multidevices/uwb/AndroidTest.xml
deleted file mode 100644
index d230b3a..0000000
--- a/hostsidetests/multidevices/uwb/AndroidTest.xml
+++ /dev/null
@@ -1,57 +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.
--->
-<configuration description="Config for CTS Uwb multi-device test cases">
-    <option name="test-suite-tag" value="cts" />
-    <option name="config-descriptor:metadata" key="component" value="uwb" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_secondary_user" />
-
-    <object class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController"
-        type="module_controller">
-        <option name="required-feature" value="android.hardware.uwb" />
-    </object>
-
-    <device name="device1">
-        <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-            <option name="test-file-name" value="uwb_snippet.apk" />
-        </target_preparer>
-        <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-            <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
-            <option name="run-command" value="wm dismiss-keyguard" />
-        </target_preparer>
-        <!-- Any python dependencies can be specified and will be installed with pip -->
-        <!-- TODO(b/225958696): Import python dependencies -->
-        <target_preparer class="com.android.tradefed.targetprep.PythonVirtualenvPreparer">
-          <option name="dep-module" value="mobly" />
-        </target_preparer>
-        -->
-    </device>
-    <device name="device2">
-        <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-            <option name="test-file-name" value="uwb_snippet.apk" />
-        </target_preparer>
-        <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
-            <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
-            <option name="run-command" value="wm dismiss-keyguard" />
-        </target_preparer>
-    </device>
-
-    <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest">
-      <!-- The mobly-par-file-name should match the module name -->
-      <option name="mobly-par-file-name" value="CtsUwbMultiDeviceTestCases" />
-      <!-- Timeout limit in milliseconds for all test cases of the python binary -->
-      <option name="mobly-test-timeout" value="60000" />
-    </test>
-</configuration>
-
diff --git a/hostsidetests/multidevices/uwb/OWNERS b/hostsidetests/multidevices/uwb/OWNERS
deleted file mode 100644
index ea05fdf..0000000
--- a/hostsidetests/multidevices/uwb/OWNERS
+++ /dev/null
@@ -1,7 +0,0 @@
-# Bug component: 33618
-include platform/packages/modules/Uwb:/OWNERS
-
-# Engprod - Not owner of the test but help maintaining the module as an example
-jdesprez@google.com
-frankfeng@google.com
-murj@google.com
diff --git a/hostsidetests/multidevices/uwb/snippet/AndroidManifest.xml b/hostsidetests/multidevices/uwb/snippet/AndroidManifest.xml
deleted file mode 100644
index 6b28be9..0000000
--- a/hostsidetests/multidevices/uwb/snippet/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.google.snippet.uwb">
-
-  <uses-permission android:name="android.permissions.UWB_PRIVILEGED" />
-  <uses-permission android:name="android.permission.UWB_RANGING" />
-
-  <application>
-    <!-- Add any classes that implement the Snippet interface as meta-data, whose
-         value is a comma-separated string, each section being the package path
-         of a snippet class -->
-    <meta-data
-        android:name="mobly-snippets"
-        android:value="com.google.snippet.uwb.UwbManagerSnippet" />
-  </application>
-  <!-- Add an instrumentation tag so that the app can be launched through an
-       instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
-       is derived from `AndroidJUnitRunner`, and is required to use the
-       Mobly Snippet Lib. -->
-  <instrumentation
-      android:name="com.google.android.mobly.snippet.SnippetRunner"
-      android:targetPackage="com.google.snippet.uwb" />
-</manifest>
diff --git a/hostsidetests/multidevices/uwb/snippet/UwbManagerSnippet.java b/hostsidetests/multidevices/uwb/snippet/UwbManagerSnippet.java
deleted file mode 100644
index 484d8cb..0000000
--- a/hostsidetests/multidevices/uwb/snippet/UwbManagerSnippet.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 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.google.snippet.uwb;
-
-import android.app.UiAutomation;
-import android.content.Context;
-import android.uwb.UwbManager;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.google.android.mobly.snippet.Snippet;
-import com.google.android.mobly.snippet.rpc.Rpc;
-import com.google.android.mobly.snippet.util.Log;
-
-import java.lang.reflect.Method;
-
-/** Snippet class exposing Android APIs for Uwb. */
-public class UwbManagerSnippet implements Snippet {
-    private static class UwbManagerSnippetException extends Exception {
-        private static final long serialVersionUID = 1;
-
-        UwbManagerSnippetException(String msg) {
-            super(msg);
-        }
-
-        UwbManagerSnippetException(String msg, Throwable err) {
-            super(msg, err);
-        }
-    }
-    private final UwbManager mUwbManager;
-    private final Context mContext;
-
-    public UwbManagerSnippet() throws Throwable {
-        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-        mUwbManager = mContext.getSystemService(UwbManager.class);
-        adoptShellPermission();
-    }
-
-    /** Get the UWB state. */
-    @Rpc(description = "Get Uwb state")
-    public boolean isUwbEnabled() {
-        return mUwbManager.isUwbEnabled();
-    }
-
-    /** Get the UWB state. */
-    @Rpc(description = "Set Uwb state")
-    public void setUwbEnabled(boolean enabled) {
-        mUwbManager.setUwbEnabled(enabled);
-    }
-
-    @Override
-    public void shutdown() {}
-
-    private void adoptShellPermission() throws Throwable {
-        UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation();
-        uia.adoptShellPermissionIdentity();
-        try {
-            Class<?> cls = Class.forName("android.app.UiAutomation");
-            Method destroyMethod = cls.getDeclaredMethod("destroy");
-            destroyMethod.invoke(uia);
-        } catch (ReflectiveOperationException e) {
-            throw new UwbManagerSnippetException("Failed to cleaup Ui Automation", e);
-        }
-    }
-}
diff --git a/hostsidetests/multidevices/uwb/uwb_manager_test.py b/hostsidetests/multidevices/uwb/uwb_manager_test.py
deleted file mode 100644
index abff616..0000000
--- a/hostsidetests/multidevices/uwb/uwb_manager_test.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# Lint as: python3
-"""Porting UWB tests from mobly."""
-import sys
-import time
-
-import logging
-logging.basicConfig(filename="/tmp/uwb_test_log.txt", level=logging.INFO)
-
-from mobly import asserts
-from mobly import base_test
-from mobly import test_runner
-from mobly import utils
-from mobly.controllers import android_device
-from pprint import pprint
-
-UWB_SNIPPET_PACKAGE = 'com.google.snippet.uwb'
-
-class UwbManagerTest(base_test.BaseTestClass):
-
-  def setup_class(self):
-    # Declare that two Android devices are needed.
-    self.initiator, self.responder = self.register_controller(
-        android_device, min_number=2)
-
-    def setup_device(device):
-      # Expect uwb apk to be installed as it is configured to install
-      # with the module configuration AndroidTest.xml on both devices.
-      device.adb.shell([
-          'pm', 'grant', UWB_SNIPPET_PACKAGE,
-          'android.permission.UWB_RANGING'
-      ])
-      device.load_snippet('uwb_snippet', UWB_SNIPPET_PACKAGE)
-
-    # Sets up devices in parallel to save time.
-    utils.concurrent_exec(
-        setup_device, ((self.initiator,), (self.responder,)),
-        max_workers=2,
-        raise_on_exception=True)
-
-
-  def test_default_uwb_state(self):
-    """Verifies default UWB state is On after flashing the device."""
-    asserts.assert_true(self.initiator.uwb_snippet.isUwbEnabled(),
-                        "UWB state: Off; Expected: On.")
-    asserts.assert_true(self.responder.uwb_snippet.isUwbEnabled(),
-                        "UWB state: Off; Expected: On.")
-
-  def test_uwb_toggle(self):
-      """Verifies UWB toggle on/off """
-      self.initiator.uwb_snippet.setUwbEnabled(False)
-      self.responder.uwb_snippet.setUwbEnabled(False)
-      # TODO: Use callback to wait for toggle off completion.
-      time.sleep(5)
-      asserts.assert_false(self.initiator.uwb_snippet.isUwbEnabled(),
-                          "UWB state: Off; Expected: On.")
-      asserts.assert_false(self.responder.uwb_snippet.isUwbEnabled(),
-                          "UWB state: Off; Expected: On.")
-
-      self.initiator.uwb_snippet.setUwbEnabled(True)
-      self.responder.uwb_snippet.setUwbEnabled(True)
-      # TODO: Use callback to wait for toggle off completion.
-      time.sleep(5)
-      asserts.assert_true(self.initiator.uwb_snippet.isUwbEnabled(),
-                          "UWB state: Off; Expected: On.")
-      asserts.assert_true(self.responder.uwb_snippet.isUwbEnabled(),
-                          "UWB state: Off; Expected: On.")
-
-if __name__ == '__main__':
-  # Take test args
-  index = sys.argv.index('--')
-  sys.argv = sys.argv[:1] + sys.argv[index + 1:]
-
-  test_runner.main()
diff --git a/hostsidetests/scopedstorage/Android.bp b/hostsidetests/scopedstorage/Android.bp
index 4453beb..d5052b9 100644
--- a/hostsidetests/scopedstorage/Android.bp
+++ b/hostsidetests/scopedstorage/Android.bp
@@ -91,7 +91,7 @@
     // Tag as a CTS artifact
     test_suites: [
         "general-tests",
-        "mts",
+        "mts-mediaprovider",
         "cts",
     ],
 }
@@ -155,7 +155,7 @@
     // Tag as a CTS artifact
     test_suites: [
         "general-tests",
-        "mts",
+        "mts-mediaprovider",
         "cts",
     ],
 }
@@ -171,7 +171,7 @@
     // Tag as a CTS artifact
     test_suites: [
         "general-tests",
-        "mts",
+        "mts-mediaprovider",
         "cts",
     ],
 }
@@ -187,7 +187,7 @@
     // Tag as a CTS artifact
     test_suites: [
         "general-tests",
-        "mts",
+        "mts-mediaprovider",
         "cts",
     ],
 }
@@ -238,6 +238,29 @@
 }
 
 android_test_helper_app {
+    name: "AppCloningDeviceTest",
+    manifest: "AndroidManifest.xml",
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "truth-prebuilt",
+        "cts-scopedstorage-lib",
+        "modules-utils-build_system",
+    ],
+    compile_multilib: "both",
+    test_suites: [
+        "general-tests",
+        "mts-mediaprovider",
+        "cts",
+    ],
+    sdk_version: "test_current",
+    target_sdk_version: "33",
+    min_sdk_version: "29",
+    java_resources: [
+        ":CtsScopedStorageTestAppB",
+    ],
+}
+
+android_test_helper_app {
     name: "LegacyStorageTest",
     manifest: "legacy/AndroidManifest.xml",
     srcs: ["legacy/src/**/*.java"],
@@ -303,7 +326,6 @@
         "general-tests",
         "mts-mediaprovider",
         "cts",
-        "gts"
     ],
     test_config: "AndroidTest.xml",
     per_testcase_directory: true,
@@ -319,6 +341,31 @@
 }
 
 java_test_host {
+    name: "CtsAppCloningMediaProviderHostTest",
+    srcs: ["host/src/**/*.java"],
+    libs: [
+        "cts-tradefed",
+        "tradefed",
+        "testng",
+    ],
+    static_libs: [
+        "modules-utils-build-testing",
+        "compatibility-host-util",
+    ],
+    test_suites: [
+        "general-tests",
+        "mts-mediaprovider",
+        "cts",
+    ],
+    test_config: "AndroidTestAppCloning.xml",
+    per_testcase_directory: true,
+    data: [
+        ":CtsScopedStorageTestAppB",
+        ":AppCloningDeviceTest",
+    ],
+}
+
+java_test_host {
     name: "GtsPreserveLegacyStorageHostTest",
     srcs: [
         "host/src/android/scopedstorage/cts/host/PreserveLegacyStorageHostTest.java",
diff --git a/hostsidetests/scopedstorage/AndroidManifest.xml b/hostsidetests/scopedstorage/AndroidManifest.xml
index e6902c7..39dc12e 100644
--- a/hostsidetests/scopedstorage/AndroidManifest.xml
+++ b/hostsidetests/scopedstorage/AndroidManifest.xml
@@ -17,7 +17,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.scopedstorage.cts" >
 
-    <uses-sdk android:minSdkVersion="29" />
+    <uses-sdk android:minSdkVersion="30" />
 
     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
     <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
diff --git a/hostsidetests/scopedstorage/AndroidTest.xml b/hostsidetests/scopedstorage/AndroidTest.xml
index 12e4e16..320d541 100644
--- a/hostsidetests/scopedstorage/AndroidTest.xml
+++ b/hostsidetests/scopedstorage/AndroidTest.xml
@@ -15,7 +15,6 @@
 -->
 <configuration description="External storage host test for legacy and scoped storage">
     <option name="test-suite-tag" value="cts" />
-    <option name="test-suite-tag" value="gts" />
     <option name="config-descriptor:metadata" key="component" value="framework" />
     <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
diff --git a/hostsidetests/scopedstorage/AndroidTestAppCloning.xml b/hostsidetests/scopedstorage/AndroidTestAppCloning.xml
new file mode 100644
index 0000000..665db66
--- /dev/null
+++ b/hostsidetests/scopedstorage/AndroidTestAppCloning.xml
@@ -0,0 +1,36 @@
+<?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.
+  -->
+<configuration description="External storage host test Test for App cloning use case">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="framework" />
+    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+    <option
+        name="config-descriptor:metadata"
+        key="mainline-param"
+        value="com.google.android.mediaprovider.apex" />
+
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="class" value="android.scopedstorage.cts.host.AppCloningMediaProviderHostTest" />
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
+    </object>
+</configuration>
diff --git a/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java b/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
index 717cf95..6fbf5f3 100644
--- a/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
+++ b/hostsidetests/scopedstorage/ScopedStorageTestHelper/src/android/scopedstorage/cts/ScopedStorageTestHelper.java
@@ -16,7 +16,7 @@
 package android.scopedstorage.cts;
 
 import static android.scopedstorage.cts.lib.RedactionTestHelper.EXIF_METADATA_QUERY;
-import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadata;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromFile;
 import static android.scopedstorage.cts.lib.TestUtils.CAN_OPEN_FILE_FOR_READ_QUERY;
 import static android.scopedstorage.cts.lib.TestUtils.CAN_OPEN_FILE_FOR_WRITE_QUERY;
 import static android.scopedstorage.cts.lib.TestUtils.CAN_READ_WRITE_QUERY;
@@ -253,7 +253,7 @@
         if (getIntent().hasExtra(INTENT_EXTRA_PATH)) {
             final String filePath = getIntent().getStringExtra(INTENT_EXTRA_PATH);
             if (EXIF_METADATA_QUERY.equals(queryType)) {
-                intent.putExtra(queryType, getExifMetadata(new File(filePath)));
+                intent.putExtra(queryType, getExifMetadataFromFile(new File(filePath)));
             }
         } else {
             throw new IllegalStateException(
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 2fa90ba..be667c6 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/ScopedStorageDeviceTest.java
@@ -21,7 +21,7 @@
 import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
 import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMatch;
 import static android.scopedstorage.cts.lib.RedactionTestHelper.assertExifMetadataMismatch;
-import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadata;
+import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromFile;
 import static android.scopedstorage.cts.lib.RedactionTestHelper.getExifMetadataFromRawResource;
 import static android.scopedstorage.cts.lib.TestUtils.BYTES_DATA2;
 import static android.scopedstorage.cts.lib.TestUtils.STR_DATA2;
@@ -79,6 +79,8 @@
 import static android.scopedstorage.cts.lib.TestUtils.isAppInstalled;
 import static android.scopedstorage.cts.lib.TestUtils.listAs;
 import static android.scopedstorage.cts.lib.TestUtils.openWithMediaProvider;
+import static android.scopedstorage.cts.lib.TestUtils.pollForPermission;
+import static android.scopedstorage.cts.lib.TestUtils.queryAudioFile;
 import static android.scopedstorage.cts.lib.TestUtils.queryFile;
 import static android.scopedstorage.cts.lib.TestUtils.queryFileExcludingPending;
 import static android.scopedstorage.cts.lib.TestUtils.queryImageFile;
@@ -134,6 +136,7 @@
 import android.os.storage.StorageManager;
 import android.provider.DocumentsContract;
 import android.provider.MediaStore;
+import android.scopedstorage.cts.lib.RedactionTestHelper;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.StructStat;
@@ -876,7 +879,7 @@
                 // EXIF tags and might misleadingly think there are not tags to redact
                 out.getFD().sync();
 
-                HashMap<String, String> exif = getExifMetadata(jpgFile);
+                HashMap<String, String> exif = getExifMetadataFromFile(jpgFile);
                 assertExifMetadataMatch(exif, originalExif);
 
                 HashMap<String, String> exifFromTestApp =
@@ -1132,6 +1135,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);
     }
@@ -1238,7 +1246,7 @@
             assertThat(fileInPodcastsDirLowerCase.createNewFile()).isTrue();
         } finally {
             fileInPodcastsDirLowerCase.delete();
-            deleteAsLegacyApp(podcastsDirLowerCase);
+            deleteRecursivelyAsLegacyApp(podcastsDirLowerCase);
             podcastsDir.mkdirs();
         }
     }
@@ -1313,13 +1321,13 @@
                     getExifMetadataFromRawResource(R.raw.img_with_metadata);
             try (InputStream in =
                          getContext().getResources().openRawResource(R.raw.img_with_metadata);
-                FileOutputStream out = new FileOutputStream(imgFile)) {
+                 FileOutputStream out = new FileOutputStream(imgFile)) {
                 // Dump the image we have to external storage
                 FileUtils.copy(in, out);
                 // Sync file to disk to ensure file is fully written to the lower fs.
                 out.getFD().sync();
             }
-            HashMap<String, String> exif = getExifMetadata(imgFile);
+            HashMap<String, String> exif = getExifMetadataFromFile(imgFile);
             assertExifMetadataMatch(exif, originalExif);
 
             // Install test app
@@ -1327,6 +1335,8 @@
 
             // Grant A_M_L and verify access to sensitive data
             grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
+            pollForPermission(APP_C.getPackageName(),
+                    Manifest.permission.ACCESS_MEDIA_LOCATION, /* granted */ true);
             HashMap<String, String> exifFromTestApp =
                     readExifMetadataFromTestApp(APP_C, imgFile.getPath());
             assertExifMetadataMatch(exifFromTestApp, originalExif);
@@ -1336,6 +1346,8 @@
                     APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
             // revokePermission waits for permission status to be updated, but MediaProvider still
             // needs to get permission change callback and clear its permission cache.
+            pollForPermission(APP_C.getPackageName(),
+                    Manifest.permission.ACCESS_MEDIA_LOCATION, /* granted */ false);
             Thread.sleep(500);
             exifFromTestApp = readExifMetadataFromTestApp(APP_C, imgFile.getPath());
             assertExifMetadataMismatch(exifFromTestApp, originalExif);
@@ -1344,6 +1356,8 @@
             grantPermission(APP_C.getPackageName(), Manifest.permission.ACCESS_MEDIA_LOCATION);
             // grantPermission waits for permission status to be updated, but MediaProvider still
             // needs to get permission change callback and clear its permission cache.
+            pollForPermission(APP_C.getPackageName(),
+                    Manifest.permission.ACCESS_MEDIA_LOCATION, /* granted */ true);
             Thread.sleep(500);
             exifFromTestApp = readExifMetadataFromTestApp(APP_C, imgFile.getPath());
             assertExifMetadataMatch(exifFromTestApp, originalExif);
@@ -1514,6 +1528,10 @@
             // Assert we can read from the file
             assertFileContent(otherAppImageFile, BYTES_DATA1);
 
+            // Assert has access to redacted information
+            RedactionTestHelper.assertConsistentNonRedactedAccess(otherAppImageFile,
+                    R.raw.img_with_metadata);
+
             // Assert we can delete the file
             assertThat(otherAppImageFile.delete()).isTrue();
             assertThat(otherAppImageFile.exists()).isFalse();
@@ -1770,6 +1788,37 @@
     }
 
     /**
+     * Test that renaming file paths to an external directory such as Android/* and Android/* /*
+     * except Android/media/* /* is not allowed.
+     */
+    @Test
+    public void testRenameFileToAppSpecificDir() throws Exception {
+        final File testFile = new File(getExternalMediaDir(), IMAGE_FILE_NAME);
+        final File testFileNew = new File(getExternalMediaDir(), NONMEDIA_FILE_NAME);
+
+        try {
+            // Create a file in app's external media directory
+            if (!testFile.exists()) {
+                assertThat(testFile.createNewFile()).isTrue();
+            }
+
+            final String androidDirPath = getExternalStorageDir().getPath() + "/Android";
+
+            // Verify that we can't rename a file to Android/ or Android/data or
+            // Android/media directory
+            assertCantRenameFile(testFile, new File(androidDirPath, IMAGE_FILE_NAME));
+            assertCantRenameFile(testFile, new File(androidDirPath + "/data", IMAGE_FILE_NAME));
+            assertCantRenameFile(testFile, new File(androidDirPath + "/media", IMAGE_FILE_NAME));
+
+            // Verify that we can rename a file to app specific media directory.
+            assertCanRenameFile(testFile, testFileNew);
+        } finally {
+            testFile.delete();
+            testFileNew.delete();
+        }
+    }
+
+    /**
      * Test that renaming directories is allowed and aligns to default directory restrictions.
      */
     @Test
@@ -2626,8 +2675,8 @@
             // We can't rename a non-top level directory to a top level directory.
             assertCantRenameDirectory(nonTopLevelDir, topLevelDir2, null);
         } finally {
-            deleteAsLegacyApp(topLevelDir1);
-            deleteAsLegacyApp(topLevelDir2);
+            deleteRecursivelyAsLegacyApp(topLevelDir1);
+            deleteRecursivelyAsLegacyApp(topLevelDir2);
             deleteRecursively(nonTopLevelDir);
         }
     }
@@ -2637,7 +2686,7 @@
         final File podcastsDir = getPodcastsDir();
         try {
             if (podcastsDir.exists()) {
-                deleteAsLegacyApp(podcastsDir);
+                deleteRecursivelyAsLegacyApp(podcastsDir);
             }
             assertThat(podcastsDir.mkdir()).isTrue();
         } finally {
@@ -2659,7 +2708,7 @@
             if (cameraDir.exists()) {
                 // This is a work around to address a known inode cache inconsistency issue
                 // that occurs when test runs for the second time.
-                deleteAsLegacyApp(cameraDir);
+                deleteRecursivelyAsLegacyApp(cameraDir);
             }
 
             createDirectoryAsLegacyApp(cameraDir);
@@ -2683,7 +2732,7 @@
         } finally {
             deleteWithMediaProviderNoThrow(targetUri);
             deleteAsLegacyApp(nomediaFile);
-            deleteAsLegacyApp(cameraDir);
+            deleteRecursivelyAsLegacyApp(cameraDir);
         }
     }
 
@@ -2736,7 +2785,7 @@
         } finally {
             deleteAsLegacyApp(videoFile);
             deleteAsLegacyApp(nomediaFile);
-            deleteAsLegacyApp(directory);
+            deleteRecursivelyAsLegacyApp(directory);
         }
     }
 
@@ -2995,6 +3044,45 @@
         }
     }
 
+    /**
+     * Test that renaming a file to {@link Environment#DIRECTORY_RINGTONES} sets
+     * {@link MediaStore.Audio.AudioColumns#IS_RINGTONE}
+     */
+
+    @Test
+    public void testRenameToRingtoneDirectory() throws Exception {
+        final File fileInDownloads = new File(getDownloadDir(), AUDIO_FILE_NAME);
+        final File fileInRingtones = new File(getRingtonesDir(), AUDIO_FILE_NAME);
+
+        try {
+            assertThat(fileInDownloads.createNewFile()).isTrue();
+            assertThat(MediaStore.scanFile(getContentResolver(), fileInDownloads)).isNotNull();
+
+            assertCanRenameFile(fileInDownloads, fileInRingtones);
+
+            try (Cursor c = queryAudioFile(fileInRingtones,
+                    MediaStore.Audio.AudioColumns.IS_RINGTONE)) {
+                assertTrue(c.moveToFirst());
+                assertWithMessage("Expected " + MediaStore.Audio.AudioColumns.IS_RINGTONE
+                        + " to be set after renaming to " + fileInRingtones)
+                        .that(c.getInt(0)).isEqualTo(1);
+            }
+
+            assertCanRenameFile(fileInRingtones, fileInDownloads);
+
+            try (Cursor c = queryAudioFile(fileInDownloads,
+                    MediaStore.Audio.AudioColumns.IS_RINGTONE)) {
+                assertTrue(c.moveToFirst());
+                assertWithMessage("Expected " + MediaStore.Audio.AudioColumns.IS_RINGTONE
+                        + " to be unset after renaming to " + fileInDownloads)
+                        .that(c.getInt(0)).isEqualTo(0);
+            }
+        } finally {
+            fileInDownloads.delete();
+            fileInRingtones.delete();
+        }
+    }
+
     @Test
     @SdkSuppress(minSdkVersion = 31, codeName = "S")
     public void testTransformsDirFileOperations() throws Exception {
diff --git a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java
index 41a47b9..4bff21b 100644
--- a/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java
+++ b/hostsidetests/scopedstorage/device/src/android/scopedstorage/cts/device/StableUrisTest.java
@@ -33,6 +33,8 @@
 import android.app.Instrumentation;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
 import android.provider.MediaStore;
 import android.scopedstorage.cts.lib.TestUtils;
 import android.util.Log;
@@ -121,7 +123,7 @@
             Log.d(TAG, "maxRowIdOfExternalDbBeforeReset:" + maxRowIdOfExternalDbBeforeReset);
 
             // Clear MediaProvider package data to trigger DB recreation.
-            mDevice.executeShellCommand("pm clear com.google.android.providers.media.module");
+            mDevice.executeShellCommand("pm clear " + getMediaProviderPackageName());
             waitForMountedAndIdleState(mContentResolver);
             MediaStore.scanVolume(mContentResolver, mVolumeName);
 
@@ -172,4 +174,11 @@
         return files;
     }
 
+    private static String getMediaProviderPackageName() {
+        final Instrumentation inst = androidx.test.InstrumentationRegistry.getInstrumentation();
+        final PackageManager packageManager = inst.getContext().getPackageManager();
+        final ProviderInfo providerInfo = packageManager.resolveContentProvider(
+                MediaStore.AUTHORITY, PackageManager.MATCH_ALL);
+        return providerInfo.packageName;
+    }
 }
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/AppCloningMediaProviderHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/AppCloningMediaProviderHostTest.java
new file mode 100644
index 0000000..77086f0
--- /dev/null
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/AppCloningMediaProviderHostTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.scopedstorage.cts.host;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.annotations.AppModeFull;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
+import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
+import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
+import com.android.tradefed.util.CommandResult;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+@AppModeFull
+public class AppCloningMediaProviderHostTest extends BaseHostTestCase{
+
+    protected static final String DEVICE_TEST_APP_PACKAGE = "android.scopedstorage.cts";
+    protected static final String DEVICE_TEST_APP = "AppCloningDeviceTest.apk";
+    private static final String DEVICE_TEST_CLASS = DEVICE_TEST_APP_PACKAGE
+            + ".AppCloningDeviceTest";
+    // This app performs the File Creation and Read operations from the Device.
+    protected static final String SCOPED_STORAGE_TEST_APP_B_APK = "CtsScopedStorageTestAppB.apk";
+
+    private static final int CLONE_PROFILE_DIRECTORY_CREATION_TIMEOUT_MS = 20000;
+    private static final long DEFAULT_INSTRUMENTATION_TIMEOUT_MS = 600_000;
+    private static final String EXTERNAL_STORAGE_PATH = "/storage/emulated/%d/";
+    private static final String CURRENT_USER_ID = "currentUserId";
+    private static final String FILE_TO_BE_CREATED = "fileToBeCreated";
+    private static final String FILE_EXPECTED_TO_BE_PRESENT = "fileExpectedToBePresent";
+    private static final String FILE_NOT_EXPECTED_TO_BE_PRESENT = "fileNotExpectedToBePresent";
+    /**
+     * Provide different name to Files being created, on each execution of the test, so that
+     * flakiness from previously existing files can be avoided.
+     */
+    private static final String NONCE = String.valueOf(System.nanoTime());
+    private static String sCloneUserId;
+
+    @BeforeClassWithInfo
+    public static void beforeClassWithDevice(TestInformation testInfo) throws Exception {
+        final ITestDevice sDevice = testInfo.getDevice();
+        assertThat(sDevice).isNotNull();
+
+        setDevice(sDevice);
+
+        assumeTrue("Device doesn't support multiple users", supportsMultipleUsers());
+        assumeFalse("Device is in headless system user mode", isHeadlessSystemUserMode());
+        assumeTrue(isAtLeastS());
+        assumeFalse("Device uses sdcardfs", usesSdcardFs());
+
+        // create clone user
+        String output = sDevice.executeShellCommand(
+                "pm create-user --profileOf 0 --user-type android.os.usertype.profile.CLONE "
+                        + "testUser");
+        sCloneUserId = output.substring(output.lastIndexOf(' ') + 1).replaceAll("[^0-9]",
+                "");
+        assertThat(sCloneUserId).isNotEmpty();
+        // start clone user
+        CommandResult out = sDevice.executeShellV2Command("am start-user -w " + sCloneUserId);
+        assertThat(isSuccessful(out)).isTrue();
+
+        Integer mCloneUserIdInt = Integer.parseInt(sCloneUserId);
+        String sCloneUserStoragePath = String.format(EXTERNAL_STORAGE_PATH,
+                Integer.parseInt(sCloneUserId));
+        // Check that the clone user directories have been created
+        eventually(() -> sDevice.doesFileExist(sCloneUserStoragePath, mCloneUserIdInt),
+                CLONE_PROFILE_DIRECTORY_CREATION_TIMEOUT_MS);
+    }
+
+    @AfterClassWithInfo
+    public static void afterClass(TestInformation testInfo) throws Exception {
+        if (!supportsMultipleUsers() || isHeadlessSystemUserMode() || !isAtLeastS()
+                || usesSdcardFs()) return;
+        testInfo.getDevice().executeShellCommand("pm remove-user " + sCloneUserId);
+    }
+
+    @Test
+    public void testGetFilesInDirectoryViaMediaProvider() throws Exception {
+        // Install the Device Test App in both the user spaces.
+        installPackage(DEVICE_TEST_APP, "--user all");
+        // Install the Scoped Storage Test App in both the user spaces.
+        installPackage(SCOPED_STORAGE_TEST_APP_B_APK, "--user all");
+
+        int currentUserId = getCurrentUserId();
+        final String fileName = "tmpFileToPush" + NONCE + ".png";
+
+        // We add the file in DCIM directory of User 0.
+        Map<String, String> ownerArgs = new HashMap<>();
+        ownerArgs.put(CURRENT_USER_ID, String.valueOf(currentUserId));
+        ownerArgs.put(FILE_TO_BE_CREATED, fileName);
+        runDeviceTestAsUserInPkgA("testInsertFilesInDirectoryViaMediaProvider",
+                currentUserId, ownerArgs);
+
+        // We add the file in DCIM directory of Cloned User.
+        final String fileNameClonedUser = "tmpFileToPushClonedUser" + NONCE + ".png";
+        Map<String, String> cloneArgs = new HashMap<>();
+        cloneArgs.put(CURRENT_USER_ID, sCloneUserId);
+        cloneArgs.put(FILE_TO_BE_CREATED, fileNameClonedUser);
+        runDeviceTestAsUserInPkgA("testInsertFilesInDirectoryViaMediaProvider",
+                Integer.parseInt(sCloneUserId), cloneArgs);
+
+        // Querying as user 0 should enlist the file(s) created by user 0 only.
+        Map<String, String> listFilesArgs = new HashMap<>();
+        listFilesArgs.put(CURRENT_USER_ID, String.valueOf(currentUserId));
+        listFilesArgs.put(FILE_EXPECTED_TO_BE_PRESENT, fileName);
+        listFilesArgs.put(FILE_NOT_EXPECTED_TO_BE_PRESENT, fileNameClonedUser);
+        runDeviceTestAsUserInPkgA("testGetFilesInDirectoryViaMediaProviderRespectsUserId",
+                currentUserId, listFilesArgs);
+
+        // Querying as cloned user should enlist the file(s) created by cloned user only.
+        listFilesArgs.put(CURRENT_USER_ID, sCloneUserId);
+        listFilesArgs.put(FILE_EXPECTED_TO_BE_PRESENT, fileNameClonedUser);
+        listFilesArgs.put(FILE_NOT_EXPECTED_TO_BE_PRESENT, fileName);
+        runDeviceTestAsUserInPkgA("testGetFilesInDirectoryViaMediaProviderRespectsUserId",
+                Integer.parseInt(sCloneUserId), listFilesArgs);
+    }
+
+    protected void runDeviceTestAsUserInPkgA(@Nonnull String testMethod, int userId,
+            @Nonnull Map<String, String> args) throws Exception {
+        DeviceTestRunOptions deviceTestRunOptions =
+                new DeviceTestRunOptions(DEVICE_TEST_APP_PACKAGE)
+                .setDevice(getDevice())
+                .setTestClassName(DEVICE_TEST_CLASS)
+                .setTestMethodName(testMethod)
+                .setMaxInstrumentationTimeoutMs(DEFAULT_INSTRUMENTATION_TIMEOUT_MS)
+                .setUserId(userId);
+        for (Map.Entry<String, String> entry : args.entrySet()) {
+            deviceTestRunOptions.addInstrumentationArg(entry.getKey(), entry.getValue());
+        }
+        assertWithMessage(testMethod + " failed").that(
+                runDeviceTests(deviceTestRunOptions)).isTrue();
+    }
+}
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/BaseHostTestCase.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/BaseHostTestCase.java
index 5587a88..6bc7328 100644
--- a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/BaseHostTestCase.java
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/BaseHostTestCase.java
@@ -16,6 +16,8 @@
 
 package android.scopedstorage.cts.host;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.NativeDevice;
@@ -28,6 +30,11 @@
 abstract class BaseHostTestCase extends BaseHostJUnit4Test {
     private int mCurrentUserId = NativeDevice.INVALID_USER_ID;
     private static final String ERROR_MESSAGE_TAG = "[ERROR]";
+    protected static ITestDevice sDevice = null;
+
+    protected static void setDevice(ITestDevice device) {
+        sDevice = device;
+    }
 
     protected String executeShellCommand(String cmd, Object... args) throws Exception {
         return getDevice().executeShellCommand(String.format(cmd, args));
@@ -42,14 +49,14 @@
     }
 
     // TODO (b/174775905) remove after exposing the check from ITestDevice.
-    protected boolean isHeadlessSystemUserMode() throws DeviceNotAvailableException {
-        String result = getDevice()
+    protected static boolean isHeadlessSystemUserMode() throws DeviceNotAvailableException {
+        String result = sDevice
                 .executeShellCommand("getprop ro.fw.mu.headless_system_user").trim();
         return "true".equalsIgnoreCase(result);
     }
 
-    protected boolean isAtLeastS() throws DeviceNotAvailableException {
-        return getDevice().getApiLevel() >= 31 /* BUILD.VERSION_CODES.S */;
+    protected static boolean isAtLeastS() throws DeviceNotAvailableException {
+        return sDevice.getApiLevel() >= 31 /* BUILD.VERSION_CODES.S */;
     }
 
     protected static void eventually(ThrowingRunnable r, long timeoutMillis) {
@@ -79,7 +86,7 @@
         return mCurrentUserId;
     }
 
-    protected boolean isSuccessful(CommandResult result) {
+    protected static boolean isSuccessful(CommandResult result) {
         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
             return false;
         }
@@ -91,6 +98,22 @@
         return (stderr == null || stderr.trim().isEmpty());
     }
 
+    protected static boolean supportsMultipleUsers() throws DeviceNotAvailableException {
+        return sDevice.getMaxNumberOfUsersSupported() > 1;
+    }
+
+    protected static boolean usesSdcardFs() throws Exception {
+        CommandResult out = sDevice.executeShellV2Command("cat /proc/mounts");
+        assertThat(isSuccessful(out)).isTrue();
+        for (String line : out.getStdout().split("\n")) {
+            String[] split = line.split(" ");
+            if (split.length >= 3 && split[2].equals("sdcardfs")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void setCurrentUserId() throws Exception {
         if (mCurrentUserId != NativeDevice.INVALID_USER_ID) return;
 
diff --git a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
index 108462b..7590679 100644
--- a/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
+++ b/hostsidetests/scopedstorage/host/src/android/scopedstorage/cts/host/ScopedStorageHostTest.java
@@ -18,20 +18,15 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assume.assumeTrue;
-
 import android.platform.test.annotations.AppModeFull;
 
-import com.android.compatibility.common.util.CtsDownstreamingTest;
-import com.android.modules.utils.build.testing.DeviceSdkLevel;
-import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -74,9 +69,6 @@
 
     @Before
     public void setup() throws Exception {
-        DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(getDevice());
-        assumeTrue(deviceSdkLevel.isDeviceAtLeastR());
-
         setupExternalStorage();
         executeShellCommand("mkdir /sdcard/Android/data/com.android.shell -m 2770");
         executeShellCommand("mkdir /sdcard/Android/data/com.android.shell/files -m 2770");
@@ -104,6 +96,16 @@
     }
 
     @Test
+    public void testManageExternalStorageCanReadRedactedContents() throws Exception {
+        allowAppOps("android:manage_external_storage");
+        try {
+            runDeviceTest("testManageExternalStorageCanReadRedactedContents");
+        } finally {
+            denyAppOps("android:manage_external_storage");
+        }
+    }
+
+    @Test
     public void testManageExternalStorageCanRenameOtherAppsContents() throws Exception {
         allowAppOps("android:manage_external_storage");
         try {
@@ -154,48 +156,26 @@
     }
 
     @Test
-    @CtsDownstreamingTest
     public void testCheckInstallerAppAccessToObbDirs() throws Exception {
-        CLog.i("IF THIS TEST FAILS GTS, please check if your build includes "
-                + "the following critical Android T patch: "
-                + "https://docs.partner.android.com/gms/policies/bulletins/sep-2022#239495492");
-
         allowAppOps("android:request_install_packages");
-        // WRITE_EXTERNAL_STORAGE is no-op for Installers T onwards
-        if (isSdkLevelLessThanT()) {
-            grantPermissions("android.permission.WRITE_EXTERNAL_STORAGE");
-        }
+        grantPermissions("android.permission.WRITE_EXTERNAL_STORAGE");
         try {
             runDeviceTest("testCheckInstallerAppAccessToObbDirs");
         } finally {
             denyAppOps("android:request_install_packages");
-            // WRITE_EXTERNAL_STORAGE is no-op for Installers T onwards
-            if (isSdkLevelLessThanT()) {
-                revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
-            }
+            revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
         }
     }
 
     @Test
-    @CtsDownstreamingTest
     public void testCheckInstallerAppCannotAccessDataDirs() throws Exception {
-        CLog.i("IF THIS TEST FAILS GTS, please check if your build includes "
-                + "the following critical Android T patch: "
-                + "https://docs.partner.android.com/gms/policies/bulletins/sep-2022#239495492");
-
         allowAppOps("android:request_install_packages");
-        // WRITE_EXTERNAL_STORAGE is no-op for Installers T onwards
-        if (isSdkLevelLessThanT()) {
-            grantPermissions("android.permission.WRITE_EXTERNAL_STORAGE");
-        }
+        grantPermissions("android.permission.WRITE_EXTERNAL_STORAGE");
         try {
             runDeviceTest("testCheckInstallerAppCannotAccessDataDirs");
         } finally {
             denyAppOps("android:request_install_packages");
-            // WRITE_EXTERNAL_STORAGE is no-op for Installers T onwards
-            if (isSdkLevelLessThanT()) {
-                revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
-            }
+            revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE");
         }
     }
 
@@ -331,6 +311,7 @@
     }
 
     @Test
+    @Ignore("b/247099819")
     public void testClearPackageData() throws Exception {
         grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
         try {
@@ -420,9 +401,4 @@
             executeShellCommand("cmd appops set --uid android.scopedstorage.cts " + op + " deny");
         }
     }
-
-    private boolean isSdkLevelLessThanT() throws DeviceNotAvailableException {
-        DeviceSdkLevel deviceSdkLevel = new DeviceSdkLevel(getDevice());
-        return !deviceSdkLevel.isDeviceAtLeastT();
-    }
 }
diff --git a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java
index b7509e6..ee0d203 100644
--- a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/RedactionTestHelper.java
@@ -21,11 +21,15 @@
 import static org.junit.Assert.fail;
 
 import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.FileUtils;
+import android.provider.MediaStore;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RawRes;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
@@ -56,12 +60,26 @@
      * Retrieve the EXIF metadata from the given file.
      */
     @NonNull
-    public static HashMap<String, String> getExifMetadata(@NonNull File file) throws IOException {
+    public static HashMap<String, String> getExifMetadataFromFile(@NonNull File file)
+            throws IOException {
         final ExifInterface exif = new ExifInterface(file);
         return dumpExifGpsTagsToMap(exif);
     }
 
     /**
+     * Retrieve the EXIF metadata from the given uri.
+     */
+    @NonNull
+    private static HashMap<String, String> getExifMetadataFromUri(@NonNull Uri uri)
+            throws IOException {
+        try (InputStream is = getContext().getContentResolver().openInputStream(uri)) {
+            final ExifInterface exif = new ExifInterface(is);
+            return dumpExifGpsTagsToMap(exif);
+        }
+    }
+
+
+    /**
      * Retrieve the EXIF metadata from the given resource.
      */
     @NonNull
@@ -116,4 +134,26 @@
         }
         return res;
     }
+
+    public static void assertConsistentNonRedactedAccess(File file, int metadataResId)
+            throws Exception {
+        // Write some meta-data to the file to assert on redacted information access
+        try (InputStream in =
+                     getContext().getResources().openRawResource(metadataResId);
+             FileOutputStream out = new FileOutputStream(file)) {
+            FileUtils.copy(in, out);
+            out.getFD().sync();
+        }
+
+        HashMap<String, String> originalExif = getExifMetadataFromRawResource(metadataResId);
+
+        // Using File API
+        HashMap<String, String> exifFromFilePath = getExifMetadataFromFile(file);
+        assertExifMetadataMatch(exifFromFilePath, originalExif);
+
+        Uri uri = MediaStore.scanFile(getContext().getContentResolver(), file);
+        // Using ContentResolver API
+        HashMap<String, String> exifFromContentResolver = getExifMetadataFromUri(uri);
+        assertExifMetadataMatch(exifFromContentResolver, originalExif);
+    }
 }
diff --git a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
index cb5fc8d..f17c98f 100644
--- a/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
+++ b/hostsidetests/scopedstorage/libs/ScopedStorageTestLib/src/android/scopedstorage/cts/lib/TestUtils.java
@@ -751,6 +751,17 @@
     }
 
     /**
+     * Queries {@link ContentResolver} for an audio file and returns a {@link Cursor} with the given
+     * columns.
+     */
+    @NonNull
+    public static Cursor queryAudioFile(File file, String... projection) {
+        return queryFile(getContentResolver(),
+                MediaStore.Audio.Media.getContentUri(sStorageVolumeName), file,
+                /*includePending*/ true, projection);
+    }
+
+    /**
      * Queries {@link ContentResolver} for a file and returns the corresponding mime type for its
      * entry in the database.
      */
@@ -984,33 +995,23 @@
         final File otherAppExternalDataDir = new File(getExternalFilesDir().getPath().replace(
                 callingPackageName, otherApp.getPackageName()));
         final File file = new File(otherAppExternalDataDir, fileName);
+        String absolutePath = file.getAbsolutePath();
+
+        final ContentValues valuesWithRelativePath = new ContentValues();
+        final String absoluteDirectoryPath = otherAppExternalDataDir.getAbsolutePath();
+        valuesWithRelativePath.put(MediaStore.MediaColumns.RELATIVE_PATH,
+                absoluteDirectoryPath.substring(absoluteDirectoryPath.indexOf("Android")));
+        valuesWithRelativePath.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
+
         try {
             assertThat(createFileAs(otherApp, file.getPath())).isTrue();
+            assertCantInsertDataValue(throwsExceptionForDataValue, absolutePath);
+            assertCantInsertDataValue(throwsExceptionForDataValue,
+                    "/sdcard/" + absolutePath.substring(absolutePath.indexOf("Android")));
+            assertCantInsertDataValue(throwsExceptionForDataValue,
+                    "/storage/emulated/0/Pictures/../"
+                            + absolutePath.substring(absolutePath.indexOf("Android")));
 
-            final ContentValues valuesWithData = new ContentValues();
-            valuesWithData.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
-            try {
-                Uri uri = getContentResolver().insert(
-                        MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
-                        valuesWithData);
-
-                if (throwsExceptionForDataValue) {
-                    fail("File insert expected to fail: " + file);
-                } else {
-                    try (Cursor c = getContentResolver().query(uri, new String[]{
-                            MediaStore.MediaColumns.DATA}, null, null)) {
-                        assertThat(c.moveToFirst()).isTrue();
-                        assertThat(c.getString(0)).isNotEqualTo(file.getAbsolutePath());
-                    }
-                }
-            } catch (IllegalArgumentException expected) {
-            }
-
-            final ContentValues valuesWithRelativePath = new ContentValues();
-            final String path = file.getAbsolutePath();
-            valuesWithRelativePath.put(MediaStore.MediaColumns.RELATIVE_PATH,
-                    path.substring(path.indexOf("Android")));
-            valuesWithRelativePath.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
             try {
                 getContentResolver().insert(MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
                         valuesWithRelativePath);
@@ -1022,6 +1023,34 @@
         }
     }
 
+    private static void assertCantInsertDataValue(boolean throwsExceptionForDataValue,
+            String path) throws Exception {
+        if (throwsExceptionForDataValue) {
+            assertThrowsErrorOnInsertToOtherAppPrivateDirectories(path);
+        } else {
+            insertDataWithValue(path);
+            try (Cursor c = getContentResolver().query(
+                    MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
+                    new String[] {MediaStore.MediaColumns.DATA},
+                    MediaStore.MediaColumns.DATA + "=?", new String[] {path}, null)) {
+                assertThat(c.getCount()).isEqualTo(0);
+            }
+        }
+    }
+
+    private static void assertThrowsErrorOnInsertToOtherAppPrivateDirectories(String path)
+            throws Exception {
+        assertThrows(IllegalArgumentException.class, () -> insertDataWithValue(path));
+    }
+
+    private static void insertDataWithValue(String path) {
+        final ContentValues valuesWithData = new ContentValues();
+        valuesWithData.put(MediaStore.MediaColumns.DATA, path);
+
+        getContentResolver().insert(MediaStore.Files.getContentUri(VOLUME_EXTERNAL),
+                valuesWithData);
+    }
+
     /**
      * Assert that app cannot update files in other app's private directories
      *
diff --git a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/AppCloningDeviceTest.java b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/AppCloningDeviceTest.java
new file mode 100644
index 0000000..6df4c8f
--- /dev/null
+++ b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/AppCloningDeviceTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.scopedstorage.cts;
+
+import static android.scopedstorage.cts.lib.TestUtils.canOpenFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.createFileAs;
+import static android.scopedstorage.cts.lib.TestUtils.listAs;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.cts.install.lib.TestApp;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+public class AppCloningDeviceTest {
+
+    private static final String EMPTY_STRING = "";
+    private static final String EXTERNAL_STORAGE_DCIM_PATH = "/storage/emulated/%d/DCIM";
+
+    // An app with no permissions
+    private static final TestApp APP_B_NO_PERMS = new TestApp("TestAppB",
+            "android.scopedstorage.cts.testapp.B.noperms", 1, false,
+            "CtsScopedStorageTestAppB.apk");
+
+    @Test
+    public void testInsertFilesInDirectoryViaMediaProvider() throws Exception {
+        String dirPath = String.format(EXTERNAL_STORAGE_DCIM_PATH,
+                Integer.parseInt(getCurrentUserId()));
+        final File dir = new File(dirPath);
+        assertThat(dir.exists()).isTrue();
+        final File file = new File(dir, getFileToBeCreatedName());
+        assertThat(createFileAs(APP_B_NO_PERMS, file.getPath())).isTrue();
+        assertThat(canOpenFileAs(APP_B_NO_PERMS, file, true)).isTrue();
+        assertThat(listAs(APP_B_NO_PERMS, dir.getPath())).contains(file.getName());
+    }
+
+    @Test
+    public void testGetFilesInDirectoryViaMediaProviderRespectsUserId() throws Exception {
+        String dirPath = String.format(EXTERNAL_STORAGE_DCIM_PATH,
+                Integer.parseInt(getCurrentUserId()));
+        final File dir = new File(dirPath);
+        assertThat(dir.exists()).isTrue();
+        final File expectedFile = new File(dir, getFileToBeExpectedName());
+        assertThat(listAs(APP_B_NO_PERMS, dir.getPath())).contains(expectedFile.getName());
+        final File notExpectedFile = new File(dir, getFileNotToBeExpectedName());
+        assertThat(listAs(APP_B_NO_PERMS, dir.getPath())).doesNotContain(notExpectedFile.getName());
+    }
+
+    private String getTestArgumentValueForGivenKey(String testArgumentKey) {
+        final Bundle testArguments = InstrumentationRegistry.getArguments();
+        String testArgumentValue = testArguments.getString(testArgumentKey, EMPTY_STRING);
+        return testArgumentValue;
+    }
+
+    private String getCurrentUserId() {
+        return getTestArgumentValueForGivenKey("currentUserId");
+    }
+
+    private String getFileToBeCreatedName() {
+        return getTestArgumentValueForGivenKey("fileToBeCreated");
+    }
+
+    private String getFileToBeExpectedName() {
+        return getTestArgumentValueForGivenKey("fileExpectedToBePresent");
+    }
+
+    private String getFileNotToBeExpectedName() {
+        return getTestArgumentValueForGivenKey("fileNotExpectedToBePresent");
+    }
+}
diff --git a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
index 6bcaa5a..253a7b8 100644
--- a/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
+++ b/hostsidetests/scopedstorage/src/android/scopedstorage/cts/ScopedStorageTest.java
@@ -92,6 +92,7 @@
 import android.os.storage.StorageManager;
 import android.platform.test.annotations.AppModeInstant;
 import android.provider.MediaStore;
+import android.scopedstorage.cts.lib.RedactionTestHelper;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.Log;
@@ -224,6 +225,25 @@
     }
 
     @Test
+    public void testManageExternalStorageCanReadRedactedContents() throws Exception {
+        pollForManageExternalStorageAllowed();
+
+        final File otherAppImage = new File(getDcimDir(), "other" + IMAGE_FILE_NAME);
+
+        try {
+            // Create file as another app
+            assertThat(createFileAs(APP_B_NO_PERMS, otherAppImage.getPath())).isTrue();
+
+            // Assert has access to redacted information
+            RedactionTestHelper.assertConsistentNonRedactedAccess(otherAppImage,
+                    R.raw.img_with_metadata);
+
+        } finally {
+            deleteFileAsNoThrow(APP_B_NO_PERMS, otherAppImage.getAbsolutePath());
+        }
+    }
+
+    @Test
     public void testManageExternalStorageCantReadWriteOtherAppExternalDir() throws Exception {
         pollForManageExternalStorageAllowed();
 
@@ -1131,22 +1151,24 @@
 
             executeShellCommand("pm clear --user " + getCurrentUser() + " " + testAppPackageName);
 
-            // Wait a max of 5 seconds for the cleaning after "pm clear" command to complete.
+            // Wait a max of 10 seconds for the cleaning after "pm clear" command to complete.
             int i = 0;
-            while(i < 10 && getFileRowIdFromDatabase(fileToBeDeleted) != -1
+            while (i < 20 && getFileRowIdFromDatabase(fileToBeDeleted) != -1
                 && getFileRowIdFromDatabase(nestedFileToBeDeleted) != -1) {
                 Thread.sleep(500);
                 i++;
             }
-
-            assertThat(getFileOwnerPackageFromDatabase(fileToRemain)).isNull();
-            assertThat(getFileRowIdFromDatabase(fileToRemain)).isNotEqualTo(-1);
-
-            assertThat(getFileOwnerPackageFromDatabase(fileToBeDeleted)).isNull();
             assertThat(getFileRowIdFromDatabase(fileToBeDeleted)).isEqualTo(-1);
-
-            assertThat(getFileOwnerPackageFromDatabase(nestedFileToBeDeleted)).isNull();
             assertThat(getFileRowIdFromDatabase(nestedFileToBeDeleted)).isEqualTo(-1);
+
+            // Poll for package name to be cleared for existing files
+            i = 0;
+            while (i < 20 && getFileOwnerPackageFromDatabase(fileToRemain) != null) {
+                Thread.sleep(500);
+                i++;
+            }
+            assertThat(getFileRowIdFromDatabase(fileToRemain)).isNotEqualTo(-1);
+            assertThat(getFileOwnerPackageFromDatabase(fileToRemain)).isNull();
         } finally {
             deleteFilesAs(APP_B_NO_PERMS, fileToRemain);
             deleteFilesAs(APP_B_NO_PERMS, fileToBeDeleted);
diff --git a/hostsidetests/stagedinstall/Android.bp b/hostsidetests/stagedinstall/Android.bp
index 2bab456..35850fe 100644
--- a/hostsidetests/stagedinstall/Android.bp
+++ b/hostsidetests/stagedinstall/Android.bp
@@ -605,13 +605,17 @@
 genrule {
   name: "deapexer.zip",
   tools: [
+      "blkid",
       "deapexer",
       "debugfs_static",
+      "fsck.erofs",
       "soong_zip",
   ],
   cmd: "rm -rf mkdir $(genDir)/deapexer && mkdir $(genDir)/deapexer && " +
        "cp $(location deapexer) $(genDir)/deapexer && " +
        "cp $(location debugfs_static) $(genDir)/deapexer && " +
+       "cp $(location blkid) $(genDir)/deapexer && " +
+       "cp $(location fsck.erofs) $(genDir)/deapexer && " +
        "HOST_OUT_SHARED_LIBRARIES=$$(dirname $(location deapexer))/../lib64 && " +
        "cp $${HOST_OUT_SHARED_LIBRARIES}/libc++.* $(genDir)/deapexer && " +
        "$(location soong_zip) -o $(out) -C $(genDir)/deapexer -D $(genDir)/deapexer",
diff --git a/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/ApexShimValidationTest.java b/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/ApexShimValidationTest.java
index ef03c1f..1e52abd 100644
--- a/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/ApexShimValidationTest.java
+++ b/hostsidetests/stagedinstall/src/com/android/tests/stagedinstall/host/ApexShimValidationTest.java
@@ -76,6 +76,8 @@
     private static final String DEAPEXING_FOLDER_NAME = "deapexing_";
     private static final String DEAPEXER_FILE_NAME = "deapexer";
     private static final String DEBUGFS_STATIC_FILE_NAME = "debugfs_static";
+    private static final String BLKID_FILE_NAME = "blkid";
+    private static final String FSCKEROFS_FILE_NAME = "fsck.erofs";
 
     private static final long DEFAULT_RUN_TIMEOUT_MS = 30 * 1000L;
 
@@ -148,12 +150,14 @@
         mDeapexingDir = FileUtil.createTempDir(DEAPEXING_FOLDER_NAME);
         final File deapexer = extractDeapexer(mDeapexingDir);
         final File debugfs = new File(mDeapexingDir, DEBUGFS_STATIC_FILE_NAME);
+        final File blkid = new File(mDeapexingDir, BLKID_FILE_NAME);
+        final File fsckerofs = new File(mDeapexingDir, FSCKEROFS_FILE_NAME);
         final List<File> apexes = extractApexes(mDeapexingDir);
         for (File apex : apexes) {
             final File outDir = new File(apex.getParent(), apex.getName().substring(
                     0, apex.getName().length() - APEX_FILE_SUFFIX.length()));
             try {
-                runDeapexerExtract(deapexer, debugfs, apex, outDir);
+                runDeapexerExtract(deapexer, debugfs, blkid, fsckerofs, apex, outDir);
                 final List<File> apkFiles = FileUtil.findFiles(outDir, ".+\\.apk").stream()
                         .map(str -> new File(str)).collect(Collectors.toList());
                 for (File apkFile : apkFiles) {
@@ -252,6 +256,14 @@
         assertWithMessage("Can't find " + DEBUGFS_STATIC_FILE_NAME + " binary file")
                 .that(debugfs).isNotNull();
         debugfs.setExecutable(true);
+        final File blkid = FileUtil.findFile(destDir, BLKID_FILE_NAME);
+        assertWithMessage("Can't find " + BLKID_FILE_NAME + " binary file")
+                .that(debugfs).isNotNull();
+        blkid.setExecutable(true);
+        final File fsckerofs = FileUtil.findFile(destDir, FSCKEROFS_FILE_NAME);
+        assertWithMessage("Can't find " + FSCKEROFS_FILE_NAME + " binary file")
+                .that(debugfs).isNotNull();
+        fsckerofs.setExecutable(true);
         return deapexer;
     }
 
@@ -290,7 +302,8 @@
      * @param apex The apex file to be extracted.
      * @param outDir The out folder.
      */
-    private void runDeapexerExtract(File deapexer, File debugfs, File apex, File outDir) {
+    private void runDeapexerExtract(File deapexer, File debugfs, File blkid, File fsckerofs,
+        File apex, File outDir) {
         final RunUtil runUtil = new RunUtil();
         final String os = System.getProperty("os.name").toLowerCase();
         final boolean isMacOs = (os.startsWith("mac") || os.startsWith("darwin"));
@@ -303,6 +316,10 @@
                 deapexer.getAbsolutePath(),
                 "--debugfs_path",
                 debugfs.getAbsolutePath(),
+                "--blkid_path",
+                blkid.getAbsolutePath(),
+                "--fsckerofs_path",
+                fsckerofs.getAbsolutePath(),
                 "extract",
                 apex.getAbsolutePath(),
                 outDir.getAbsolutePath());
diff --git a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/AtomTests.java b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/AtomTests.java
index 67345d0..3748db6 100644
--- a/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/AtomTests.java
+++ b/hostsidetests/statsdatom/apps/statsdapp/src/com/android/server/cts/device/statsdatom/AtomTests.java
@@ -225,6 +225,7 @@
         APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER, 118);
         APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_ACCESS_RESTRICTED_SETTINGS, 119);
         APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO, 120);
+        APP_OPS_ENUM_MAP.put(AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED, 121);
     }
 
     @Test
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java
index be665ba..7f29d44 100644
--- a/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/lib/ConfigUtils.java
@@ -69,10 +69,6 @@
                 .addAllowedLogSource("AID_SYSTEM")
                 .addAllowedLogSource("AID_BLUETOOTH")
                 .addAllowedLogSource("com.android.bluetooth")
-                // TODO(b/236681553): Remove this.
-                .addAllowedLogSource("com.android.bluetooth.services")
-                // TODO(b/236681553): Remove this.
-                .addAllowedLogSource("com.google.android.bluetooth.services")
                 .addAllowedLogSource("AID_LMKD")
                 .addAllowedLogSource("AID_MEDIA")
                 .addAllowedLogSource("AID_RADIO")
diff --git a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java
index 42b9b5c..9888e36 100644
--- a/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java
+++ b/hostsidetests/statsdatom/src/android/cts/statsdatom/statsd/UidAtomTests.java
@@ -34,13 +34,11 @@
 import com.android.os.AtomsProto.AppCrashOccurred;
 import com.android.os.AtomsProto.AppUsageEventOccurred;
 import com.android.os.AtomsProto.Atom;
-import com.android.os.AtomsProto.AttributionNode;
 import com.android.os.AtomsProto.AudioStateChanged;
 import com.android.os.AtomsProto.CameraStateChanged;
 import com.android.os.AtomsProto.FlashlightStateChanged;
 import com.android.os.AtomsProto.ForegroundServiceAppOpSessionEnded;
 import com.android.os.AtomsProto.ForegroundServiceStateChanged;
-import com.android.os.AtomsProto.GpsScanStateChanged;
 import com.android.os.AtomsProto.LmkKillOccurred;
 import com.android.os.AtomsProto.MediaCodecStateChanged;
 import com.android.os.AtomsProto.OverlayStateChanged;
@@ -49,7 +47,9 @@
 import com.android.os.AtomsProto.UiEventReported;
 import com.android.os.AtomsProto.VibratorStateChanged;
 import com.android.os.AtomsProto.WakelockStateChanged;
+import com.android.os.AttributionNode;
 import com.android.os.StatsLog.EventMetricData;
+import com.android.os.gps.GpsScanStateChanged;
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.log.LogUtil;
 import com.android.tradefed.testtype.DeviceTestCase;
diff --git a/hostsidetests/webkit/app/src/com/android/cts/webkit/WebViewDeviceSideStartupTest.java b/hostsidetests/webkit/app/src/com/android/cts/webkit/WebViewDeviceSideStartupTest.java
index 067d7da..b8d051b 100644
--- a/hostsidetests/webkit/app/src/com/android/cts/webkit/WebViewDeviceSideStartupTest.java
+++ b/hostsidetests/webkit/app/src/com/android/cts/webkit/WebViewDeviceSideStartupTest.java
@@ -26,6 +26,7 @@
 import android.webkit.SslErrorHandler;
 import android.webkit.WebView;
 import android.webkit.cts.CtsTestServer;
+import android.webkit.cts.SslMode;
 import android.webkit.cts.WebViewSyncLoader;
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
 
@@ -69,7 +70,7 @@
         }
 
         // Instant app can only have https connection.
-        CtsTestServer server = new CtsTestServer(mActivity, true);
+        CtsTestServer server = new CtsTestServer(mActivity, SslMode.NO_CLIENT_AUTH);
         final String url = server.getCookieUrl("death.html");
 
         Thread background = new Thread(new Runnable() {
diff --git a/libs/testserver/Android.bp b/libs/testserver/Android.bp
index 295c286..c4a4ecf 100644
--- a/libs/testserver/Android.bp
+++ b/libs/testserver/Android.bp
@@ -30,8 +30,13 @@
 java_library {
     name: "ctstestserver",
 
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    static_libs: [
+        "androidx.annotation_annotation",
+    ],
 
     sdk_version: "16",
-
 }
diff --git a/libs/testserver/src/android/webkit/cts/CtsTestServer.java b/libs/testserver/src/android/webkit/cts/CtsTestServer.java
index 0d3292b..a8212e7 100644
--- a/libs/testserver/src/android/webkit/cts/CtsTestServer.java
+++ b/libs/testserver/src/android/webkit/cts/CtsTestServer.java
@@ -22,6 +22,7 @@
 import android.os.Environment;
 import android.util.Base64;
 import android.util.Log;
+import android.util.Pair;
 import android.webkit.MimeTypeMap;
 
 import org.apache.http.Header;
@@ -132,14 +133,6 @@
     public static final String MESSAGE_403 = "403 forbidden";
     public static final String MESSAGE_404 = "404 not found";
 
-    public enum SslMode {
-        INSECURE,
-        NO_CLIENT_AUTH,
-        WANTS_CLIENT_AUTH,
-        NEEDS_CLIENT_AUTH,
-        TRUST_ANY_CLIENT
-    }
-
     private static Hashtable<Integer, String> sReasons;
 
     private ServerThread mServerThread;
@@ -147,11 +140,13 @@
     private AssetManager mAssets;
     private Context mContext;
     private Resources mResources;
-    private SslMode mSsl;
+    private @SslMode int mSsl;
     private MimeTypeMap mMap;
     private Vector<String> mQueries;
     private ArrayList<HttpEntity> mRequestEntities;
+    private final Map<String, Integer> mRequestCountMap = new HashMap<String, Integer>();
     private final Map<String, HttpRequest> mLastRequestMap = new HashMap<String, HttpRequest>();
+    private final Map<String, HttpResponse> mResponseMap = new HashMap<String, HttpResponse>();
     private long mDocValidity;
     private long mDocAge;
     private X509TrustManager mTrustManager;
@@ -162,7 +157,7 @@
      * @throws IOException
      */
     public CtsTestServer(Context context) throws Exception {
-        this(context, false);
+        this(context, SslMode.INSECURE);
     }
 
     public static String getReasonString(int status) {
@@ -181,7 +176,7 @@
      * @param context The application context to use for fetching assets.
      * @param ssl True if the server should be using secure sockets.
      * @throws Exception
-     */
+    */
     public CtsTestServer(Context context, boolean ssl) throws Exception {
         this(context, ssl ? SslMode.NO_CLIENT_AUTH : SslMode.INSECURE);
     }
@@ -192,7 +187,7 @@
      * @param sslMode Whether to use SSL, and if so, what client auth (if any) to use.
      * @throws Exception
      */
-    public CtsTestServer(Context context, SslMode sslMode) throws Exception {
+    public CtsTestServer(Context context, @SslMode int sslMode) throws Exception {
         this(context, sslMode, 0, 0);
     }
 
@@ -203,7 +198,7 @@
      * @param trustManager the trustManager
      * @throws Exception
      */
-    public CtsTestServer(Context context, SslMode sslMode, X509TrustManager trustManager)
+    public CtsTestServer(Context context, @SslMode int sslMode, X509TrustManager trustManager)
             throws Exception {
         this(context, sslMode, trustManager, 0, 0);
     }
@@ -216,7 +211,7 @@
      * @param certResId Raw resource ID of the server certificate to use.
      * @throws Exception
      */
-    public CtsTestServer(Context context, SslMode sslMode, int keyResId, int certResId)
+    public CtsTestServer(Context context, @SslMode int sslMode, int keyResId, int certResId)
             throws Exception {
         this(context, sslMode, new CtsTrustManager(), keyResId, certResId);
     }
@@ -230,7 +225,7 @@
      * @param certResId Raw resource ID of the server certificate to use.
      * @throws Exception
      */
-    public CtsTestServer(Context context, SslMode sslMode, X509TrustManager trustManager,
+    public CtsTestServer(Context context, @SslMode int sslMode, X509TrustManager trustManager,
             int keyResId, int certResId) throws Exception {
         mContext = context;
         mAssets = mContext.getAssets();
@@ -275,7 +270,7 @@
      * for shutdown by blindly trusting the {@link CtsTestServer}'s
      * credentials.
      */
-    private static class CtsTrustManager implements X509TrustManager {
+    static class CtsTrustManager implements X509TrustManager {
         public void checkClientTrusted(X509Certificate[] chain, String authType) {
             // Trust the CtSTestServer's client...
         }
@@ -308,6 +303,34 @@
     }
 
     /**
+     * Sets a response to be returned when a particular request path is passed in (with the option
+     * to specify additional headers).
+     *
+     * @param requestPath The path to respond to.
+     * @param responseString The response body that will be returned.
+     * @param responseHeaders Any additional headers that should be returned along with the response
+     *     (null is acceptable).
+     * @return The full URL including the path that should be requested to get the expected
+     *     response.
+     */
+    public synchronized String setResponse(
+            String requestPath, String responseString, List<Pair<String, String>> responseHeaders) {
+        HttpResponse response = createResponse(HttpStatus.SC_OK);
+        response.setEntity(createEntity(responseString));
+        if (responseHeaders != null) {
+            for (Pair<String, String> headerPair : responseHeaders) {
+                response.setHeader(headerPair.first, headerPair.second);
+            }
+        }
+        mResponseMap.put(requestPath, response);
+
+        StringBuilder sb = new StringBuilder(getBaseUri());
+        sb.append(requestPath);
+
+        return sb.toString();
+    }
+
+    /**
      * Return the URI that points to the server root.
      */
     public String getBaseUri() {
@@ -315,6 +338,15 @@
     }
 
     /**
+     * Return the absolute URL that refers to a path.
+     */
+    public String getAbsoluteUrl(String path) {
+        StringBuilder sb = new StringBuilder(getBaseUri());
+        sb.append(path);
+        return sb.toString();
+    }
+
+    /**
      * Return the absolute URL that refers to the given asset.
      * @param path The path of the asset. See {@link AssetManager#open(String)}
      */
@@ -530,11 +562,23 @@
         return mRequestEntities;
     }
 
+    /**
+     * Returns the total number of requests made.
+     */
     public synchronized int getRequestCount() {
         return mQueries.size();
     }
 
     /**
+     * Returns the number of requests made for a path.
+     */
+    public synchronized int getRequestCount(String requestPath) {
+        Integer count = mRequestCountMap.get(requestPath);
+        if (count == null) throw new IllegalArgumentException("Path not set: " + requestPath);
+        return count.intValue();
+    }
+
+    /**
      * Set the validity of any future responses in milliseconds. If this is set to a non-zero
      * value, the server will include a "Expires" header.
      * @param timeMillis The time, in milliseconds, for which any future response will be valid.
@@ -562,14 +606,25 @@
     }
 
     /**
-     * Returns the last HttpRequest at this path. Can return null if it is never requested.
+     * Returns the last HttpRequest at this path.
+     * Can return null if it is never requested.
+     *
+     * Use this method if the request you're looking for was
+     * for an asset.
      */
-    public synchronized HttpRequest getLastRequest(String requestPath) {
-        String relativeUrl = getRelativeUrl(requestPath);
-        if (!mLastRequestMap.containsKey(relativeUrl))
-            return null;
+    public HttpRequest getLastAssetRequest(String requestPath) {
+        String relativeUrl = getRelativeAssetUrl(requestPath);
         return mLastRequestMap.get(relativeUrl);
     }
+
+    /**
+     * Returns the last HttpRequest at this path.
+     * Can return null if it is never requested.
+     */
+    public HttpRequest getLastRequest(String requestPath) {
+        return mLastRequestMap.get(requestPath);
+    }
+
     /**
      * Hook for adding stuffs for HTTP POST. Default implementation does nothing.
      * @return null to use the default response mechanism of sending the requested uri as it is.
@@ -583,7 +638,7 @@
      * Return the relative URL that refers to the given asset.
      * @param path The path of the asset. See {@link AssetManager#open(String)}
      */
-    private String getRelativeUrl(String path) {
+    private String getRelativeAssetUrl(String path) {
         StringBuilder sb = new StringBuilder(ASSET_PREFIX);
         sb.append(path);
         return sb.toString();
@@ -602,6 +657,11 @@
 
         synchronized (this) {
             mQueries.add(uriString);
+            int requestCount = 0;
+            if (mRequestCountMap.containsKey(uriString)) {
+                requestCount = mRequestCountMap.get(uriString);
+            }
+            mRequestCountMap.put(uriString, requestCount + 1);
             mLastRequestMap.put(uriString, request);
             if (request instanceof HttpEntityEnclosingRequest) {
                 mRequestEntities.add(((HttpEntityEnclosingRequest)request).getEntity());
@@ -811,6 +871,12 @@
                 Log.w(TAG, "Unexpected UnsupportedEncodingException");
             }
         }
+
+        // If a response was set, it should override whatever was generated
+        if (mResponseMap.containsKey(path)) {
+            response = mResponseMap.get(path);
+        }
+
         if (response == null) {
             response = createResponse(HttpStatus.SC_NOT_FOUND);
         }
@@ -918,7 +984,7 @@
     private static class ServerThread extends Thread {
         private CtsTestServer mServer;
         private ServerSocket mSocket;
-        private SslMode mSsl;
+        private @SslMode int mSsl;
         private boolean mWillShutDown = false;
         private SSLContext mSslContext;
         private ExecutorService mExecutorService = Executors.newFixedThreadPool(20);
@@ -1005,7 +1071,7 @@
             return keyManagerFactory.getKeyManagers();
         }
 
-        ServerThread(CtsTestServer server, SslMode sslMode, InputStream key,
+        ServerThread(CtsTestServer server, @SslMode int sslMode, InputStream key,
                 InputStream cert) throws Exception {
             super("ServerThread");
             mServer = server;
diff --git a/libs/testserver/src/android/webkit/cts/SslMode.java b/libs/testserver/src/android/webkit/cts/SslMode.java
new file mode 100644
index 0000000..0da3e23
--- /dev/null
+++ b/libs/testserver/src/android/webkit/cts/SslMode.java
@@ -0,0 +1,37 @@
+/*
+ * 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.webkit.cts;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+@IntDef({
+    SslMode.INSECURE,
+    SslMode.NO_CLIENT_AUTH,
+    SslMode.WANTS_CLIENT_AUTH,
+    SslMode.NEEDS_CLIENT_AUTH,
+    SslMode.TRUST_ANY_CLIENT
+})
+public @interface SslMode {
+    int INSECURE = 0;
+    int NO_CLIENT_AUTH = 1;
+    int WANTS_CLIENT_AUTH = 2;
+    int NEEDS_CLIENT_AUTH = 3;
+    int TRUST_ANY_CLIENT = 4;
+}
diff --git a/libs/testserver/src/android/webkit/cts/TestWebServer.java b/libs/testserver/src/android/webkit/cts/TestWebServer.java
deleted file mode 100644
index 9f03939..0000000
--- a/libs/testserver/src/android/webkit/cts/TestWebServer.java
+++ /dev/null
@@ -1,625 +0,0 @@
-// Copyright 2012 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// Any changes to this file should be done in upstream chromium.org:
-// net/test/android/javatests/src/org/chromium/net/test/util/TestWebServer.java
-
-package android.webkit.cts;
-
-import android.util.Base64;
-import android.util.Log;
-import android.util.Pair;
-
-import org.apache.http.HttpException;
-import org.apache.http.HttpRequest;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.HttpVersion;
-import org.apache.http.RequestLine;
-import org.apache.http.StatusLine;
-import org.apache.http.entity.ByteArrayEntity;
-import org.apache.http.impl.DefaultHttpServerConnection;
-import org.apache.http.impl.cookie.DateUtils;
-import org.apache.http.message.BasicHttpResponse;
-import org.apache.http.params.BasicHttpParams;
-import org.apache.http.params.CoreProtocolPNames;
-import org.apache.http.params.HttpParams;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.URI;
-import java.net.URL;
-import java.net.URLConnection;
-import java.security.KeyManagementException;
-import java.security.KeyStore;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
-import javax.net.ssl.X509TrustManager;
-
-/**
- * Simple http test server for testing.
- *
- * This server runs in a thread in the current process, so it is convenient
- * for loopback testing without the need to setup tcp forwarding to the
- * host computer.
- *
- * Based heavily on the CTSWebServer in Android.
- */
-public class TestWebServer {
-    private static final String TAG = "TestWebServer";
-
-    public static final String SHUTDOWN_PREFIX = "/shutdown";
-
-    private static TestWebServer sInstance;
-    private static TestWebServer sSecureInstance;
-    private static Hashtable<Integer, String> sReasons;
-
-    private final ServerThread mServerThread;
-    private String mServerUri;
-    private final boolean mSsl;
-
-    private static class Response {
-        final byte[] mResponseData;
-        final List<Pair<String, String>> mResponseHeaders;
-        final boolean mIsRedirect;
-        final Runnable mResponseAction;
-        final boolean mIsNotFound;
-
-        Response(byte[] responseData, List<Pair<String, String>> responseHeaders,
-                boolean isRedirect, boolean isNotFound, Runnable responseAction) {
-            mIsRedirect = isRedirect;
-            mIsNotFound = isNotFound;
-            mResponseData = responseData;
-            mResponseHeaders = responseHeaders == null ?
-                    new ArrayList<Pair<String, String>>() : responseHeaders;
-            mResponseAction = responseAction;
-        }
-    }
-
-    // The Maps below are modified on both the client thread and the internal server thread, so
-    // need to use a lock when accessing them.
-    private final Object mLock = new Object();
-    private final Map<String, Response> mResponseMap = new HashMap<String, Response>();
-    private final Map<String, Integer> mResponseCountMap = new HashMap<String, Integer>();
-    private final Map<String, HttpRequest> mLastRequestMap = new HashMap<String, HttpRequest>();
-
-    /**
-     * Create and start a local HTTP server instance.
-     * @param ssl True if the server should be using secure sockets.
-     * @throws Exception
-     */
-    public TestWebServer(boolean ssl) throws Exception {
-        mSsl = ssl;
-        if (mSsl) {
-            mServerUri = "https:";
-            if (sSecureInstance != null) {
-                sSecureInstance.shutdown();
-            }
-        } else {
-            mServerUri = "http:";
-            if (sInstance != null) {
-                sInstance.shutdown();
-            }
-        }
-
-        setInstance(this, mSsl);
-        mServerThread = new ServerThread(this, mSsl);
-        mServerThread.start();
-        mServerUri += "//localhost:" + mServerThread.mSocket.getLocalPort();
-    }
-
-    /**
-     * Terminate the http server.
-     */
-    public void shutdown() {
-        try {
-            // Avoid a deadlock between two threads where one is trying to call
-            // close() and the other one is calling accept() by sending a GET
-            // request for shutdown and having the server's one thread
-            // sequentially call accept() and close().
-            URL url = new URL(mServerUri + SHUTDOWN_PREFIX);
-            URLConnection connection = openConnection(url);
-            connection.connect();
-
-            // Read the input from the stream to send the request.
-            InputStream is = connection.getInputStream();
-            is.close();
-
-            // Block until the server thread is done shutting down.
-            mServerThread.join();
-
-        } catch (MalformedURLException e) {
-            throw new IllegalStateException(e);
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        } catch (NoSuchAlgorithmException e) {
-            throw new IllegalStateException(e);
-        } catch (KeyManagementException e) {
-            throw new IllegalStateException(e);
-        }
-
-        setInstance(null, mSsl);
-    }
-
-    private static void setInstance(TestWebServer instance, boolean isSsl) {
-        if (isSsl) {
-            sSecureInstance = instance;
-        } else {
-            sInstance = instance;
-        }
-    }
-
-    private static final int RESPONSE_STATUS_NORMAL = 0;
-    private static final int RESPONSE_STATUS_MOVED_TEMPORARILY = 1;
-    private static final int RESPONSE_STATUS_NOT_FOUND = 2;
-
-    private String setResponseInternal(
-            String requestPath, byte[] responseData,
-            List<Pair<String, String>> responseHeaders, Runnable responseAction,
-            int status) {
-        final boolean isRedirect = (status == RESPONSE_STATUS_MOVED_TEMPORARILY);
-        final boolean isNotFound = (status == RESPONSE_STATUS_NOT_FOUND);
-
-        synchronized (mLock) {
-            mResponseMap.put(requestPath, new Response(
-                    responseData, responseHeaders, isRedirect, isNotFound, responseAction));
-            mResponseCountMap.put(requestPath, Integer.valueOf(0));
-            mLastRequestMap.put(requestPath, null);
-        }
-        return getResponseUrl(requestPath);
-    }
-
-    /**
-     * Gets the URL on the server under which a particular request path will be accessible.
-     *
-     * This only gets the URL, you still need to set the response if you intend to access it.
-     *
-     * @param requestPath The path to respond to.
-     * @return The full URL including the requestPath.
-     */
-    public String getResponseUrl(String requestPath) {
-        return mServerUri + requestPath;
-    }
-
-    /**
-     * Sets a 404 (not found) response to be returned when a particular request path is passed in.
-     *
-     * @param requestPath The path to respond to.
-     * @return The full URL including the path that should be requested to get the expected
-     *         response.
-     */
-    public String setResponseWithNotFoundStatus(
-            String requestPath) {
-        return setResponseInternal(requestPath, "".getBytes(), null, null,
-                RESPONSE_STATUS_NOT_FOUND);
-    }
-
-    /**
-     * Sets a response to be returned when a particular request path is passed
-     * in (with the option to specify additional headers).
-     *
-     * @param requestPath The path to respond to.
-     * @param responseString The response body that will be returned.
-     * @param responseHeaders Any additional headers that should be returned along with the
-     *                        response (null is acceptable).
-     * @return The full URL including the path that should be requested to get the expected
-     *         response.
-     */
-    public String setResponse(
-            String requestPath, String responseString,
-            List<Pair<String, String>> responseHeaders) {
-        return setResponseInternal(requestPath, responseString.getBytes(), responseHeaders, null,
-                RESPONSE_STATUS_NORMAL);
-    }
-
-    /**
-     * Sets a response to be returned when a particular request path is passed
-     * in with the option to specify additional headers as well as an arbitrary action to be
-     * executed on each request.
-     *
-     * @param requestPath The path to respond to.
-     * @param responseString The response body that will be returned.
-     * @param responseHeaders Any additional headers that should be returned along with the
-     *                        response (null is acceptable).
-     * @param responseAction The action to be performed when fetching the response.  This action
-     *                       will be executed for each request and will be handled on a background
-     *                       thread.
-     * @return The full URL including the path that should be requested to get the expected
-     *         response.
-     */
-    public String setResponseWithRunnableAction(
-            String requestPath, String responseString, List<Pair<String, String>> responseHeaders,
-            Runnable responseAction) {
-        return setResponseInternal(
-                requestPath, responseString.getBytes(), responseHeaders, responseAction,
-                RESPONSE_STATUS_NORMAL);
-    }
-
-    /**
-     * Sets a redirect.
-     *
-     * @param requestPath The path to respond to.
-     * @param targetPath The path to redirect to.
-     * @return The full URL including the path that should be requested to get the expected
-     *         response.
-     */
-    public String setRedirect(
-            String requestPath, String targetPath) {
-        List<Pair<String, String>> responseHeaders = new ArrayList<Pair<String, String>>();
-        responseHeaders.add(Pair.create("Location", targetPath));
-
-        return setResponseInternal(requestPath, targetPath.getBytes(), responseHeaders, null,
-                RESPONSE_STATUS_MOVED_TEMPORARILY);
-    }
-
-    /**
-     * Sets a base64 encoded response to be returned when a particular request path is passed
-     * in (with the option to specify additional headers).
-     *
-     * @param requestPath The path to respond to.
-     * @param base64EncodedResponse The response body that is base64 encoded. The actual server
-     *                              response will the decoded binary form.
-     * @param responseHeaders Any additional headers that should be returned along with the
-     *                        response (null is acceptable).
-     * @return The full URL including the path that should be requested to get the expected
-     *         response.
-     */
-    public String setResponseBase64(
-            String requestPath, String base64EncodedResponse,
-            List<Pair<String, String>> responseHeaders) {
-        return setResponseInternal(
-                requestPath, Base64.decode(base64EncodedResponse, Base64.DEFAULT),
-                responseHeaders, null, RESPONSE_STATUS_NORMAL);
-    }
-
-    /**
-     * Get the number of requests was made at this path since it was last set.
-     */
-    public int getRequestCount(String requestPath) {
-        Integer count = null;
-        synchronized (mLock) {
-            count = mResponseCountMap.get(requestPath);
-        }
-        if (count == null) throw new IllegalArgumentException("Path not set: " + requestPath);
-        return count.intValue();
-    }
-
-    /**
-     * Returns the last HttpRequest at this path. Can return null if it is never requested.
-     */
-    public HttpRequest getLastRequest(String requestPath) {
-        synchronized (mLock) {
-            if (!mLastRequestMap.containsKey(requestPath))
-                throw new IllegalArgumentException("Path not set: " + requestPath);
-            return mLastRequestMap.get(requestPath);
-        }
-    }
-
-    public String getBaseUrl() {
-        return mServerUri + "/";
-    }
-
-    private URLConnection openConnection(URL url)
-            throws IOException, NoSuchAlgorithmException, KeyManagementException {
-        if (mSsl) {
-            // Install hostname verifiers and trust managers that don't do
-            // anything in order to get around the client not trusting
-            // the test server due to a lack of certificates.
-
-            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
-            connection.setHostnameVerifier(new TestHostnameVerifier());
-
-            SSLContext context = SSLContext.getInstance("TLS");
-            TestTrustManager trustManager = new TestTrustManager();
-            context.init(null, new TestTrustManager[] {trustManager}, null);
-            connection.setSSLSocketFactory(context.getSocketFactory());
-
-            return connection;
-        } else {
-            return url.openConnection();
-        }
-    }
-
-    /**
-     * {@link X509TrustManager} that trusts everybody. This is used so that
-     * the client calling {@link TestWebServer#shutdown()} can issue a request
-     * for shutdown by blindly trusting the {@link TestWebServer}'s
-     * credentials.
-     */
-    private static class TestTrustManager implements X509TrustManager {
-        @Override
-        public void checkClientTrusted(X509Certificate[] chain, String authType) {
-            // Trust the TestWebServer...
-        }
-
-        @Override
-        public void checkServerTrusted(X509Certificate[] chain, String authType) {
-            // Trust the TestWebServer...
-        }
-
-        @Override
-        public X509Certificate[] getAcceptedIssuers() {
-            return null;
-        }
-    }
-
-    /**
-     * {@link HostnameVerifier} that verifies everybody. This permits
-     * the client to trust the web server and call
-     * {@link TestWebServer#shutdown()}.
-     */
-    private static class TestHostnameVerifier implements HostnameVerifier {
-        @Override
-        public boolean verify(String hostname, SSLSession session) {
-            return true;
-        }
-    }
-
-    private void servedResponseFor(String path, HttpRequest request) {
-        synchronized (mLock) {
-            mResponseCountMap.put(path, Integer.valueOf(
-                    mResponseCountMap.get(path).intValue() + 1));
-            mLastRequestMap.put(path, request);
-        }
-    }
-
-    /**
-     * Generate a response to the given request.
-     *
-     * <p>Always executed on the background server thread.
-     *
-     * <p>If there is an action associated with the response, it will be executed inside of
-     * this function.
-     *
-     * @throws InterruptedException
-     */
-    private HttpResponse getResponse(HttpRequest request) throws InterruptedException {
-        assert Thread.currentThread() == mServerThread
-                : "getResponse called from non-server thread";
-
-        RequestLine requestLine = request.getRequestLine();
-        HttpResponse httpResponse = null;
-        Log.i(TAG, requestLine.getMethod() + ": " + requestLine.getUri());
-        String uriString = requestLine.getUri();
-        URI uri = URI.create(uriString);
-        String path = uri.getPath();
-
-        Response response = null;
-        synchronized (mLock) {
-            response = mResponseMap.get(path);
-        }
-        if (path.equals(SHUTDOWN_PREFIX)) {
-            httpResponse = createResponse(HttpStatus.SC_OK);
-        } else if (response == null) {
-            httpResponse = createResponse(HttpStatus.SC_NOT_FOUND);
-        } else if (response.mIsNotFound) {
-            httpResponse = createResponse(HttpStatus.SC_NOT_FOUND);
-            servedResponseFor(path, request);
-        } else if (response.mIsRedirect) {
-            httpResponse = createResponse(HttpStatus.SC_MOVED_TEMPORARILY);
-            for (Pair<String, String> header : response.mResponseHeaders) {
-                httpResponse.addHeader(header.first, header.second);
-            }
-            servedResponseFor(path, request);
-        } else {
-            if (response.mResponseAction != null) response.mResponseAction.run();
-
-            httpResponse = createResponse(HttpStatus.SC_OK);
-            ByteArrayEntity entity = createEntity(response.mResponseData);
-            httpResponse.setEntity(entity);
-            httpResponse.setHeader("Content-Length", "" + entity.getContentLength());
-            for (Pair<String, String> header : response.mResponseHeaders) {
-                httpResponse.addHeader(header.first, header.second);
-            }
-            servedResponseFor(path, request);
-        }
-        StatusLine sl = httpResponse.getStatusLine();
-        Log.i(TAG, sl.getStatusCode() + "(" + sl.getReasonPhrase() + ")");
-        setDateHeaders(httpResponse);
-        return httpResponse;
-    }
-
-    private void setDateHeaders(HttpResponse response) {
-        response.addHeader("Date", DateUtils.formatDate(new Date(), DateUtils.PATTERN_RFC1123));
-    }
-
-    /**
-     * Create an empty response with the given status.
-     */
-    private HttpResponse createResponse(int status) {
-        HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_0, status, null);
-        String reason = null;
-
-        // This synchronized silences findbugs.
-        synchronized (TestWebServer.class) {
-            if (sReasons == null) {
-                sReasons = new Hashtable<Integer, String>();
-                sReasons.put(HttpStatus.SC_UNAUTHORIZED, "Unauthorized");
-                sReasons.put(HttpStatus.SC_NOT_FOUND, "Not Found");
-                sReasons.put(HttpStatus.SC_FORBIDDEN, "Forbidden");
-                sReasons.put(HttpStatus.SC_MOVED_TEMPORARILY, "Moved Temporarily");
-            }
-            // Fill in error reason. Avoid use of the ReasonPhraseCatalog, which is
-            // Locale-dependent.
-            reason = sReasons.get(status);
-        }
-
-        if (reason != null) {
-            StringBuffer buf = new StringBuffer("<html><head><title>");
-            buf.append(reason);
-            buf.append("</title></head><body>");
-            buf.append(reason);
-            buf.append("</body></html>");
-            ByteArrayEntity entity = createEntity(buf.toString().getBytes());
-            response.setEntity(entity);
-            response.setHeader("Content-Length", "" + entity.getContentLength());
-        }
-        return response;
-    }
-
-    /**
-     * Create a string entity for the given content.
-     */
-    private ByteArrayEntity createEntity(byte[] data) {
-        ByteArrayEntity entity = new ByteArrayEntity(data);
-        entity.setContentType("text/html");
-        return entity;
-    }
-
-    private static class ServerThread extends Thread {
-        private TestWebServer mServer;
-        private ServerSocket mSocket;
-        private boolean mIsSsl;
-        private boolean mIsCancelled;
-        private SSLContext mSslContext;
-
-        /**
-         * Defines the keystore contents for the server, BKS version. Holds just a
-         * single self-generated key. The subject name is "Test Server".
-         */
-        private static final String SERVER_KEYS_BKS =
-            "AAAAAQAAABQDkebzoP1XwqyWKRCJEpn/t8dqIQAABDkEAAVteWtleQAAARpYl20nAAAAAQAFWC41" +
-            "MDkAAAJNMIICSTCCAbKgAwIBAgIESEfU1jANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJVUzET" +
-            "MBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEBxMDTVRWMQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNV" +
-            "BAsTB0FuZHJvaWQxFDASBgNVBAMTC1Rlc3QgU2VydmVyMB4XDTA4MDYwNTExNTgxNFoXDTA4MDkw" +
-            "MzExNTgxNFowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAcTA01U" +
-            "VjEPMA0GA1UEChMGR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRQwEgYDVQQDEwtUZXN0IFNlcnZl" +
-            "cjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0LIdKaIr9/vsTq8BZlA3R+NFWRaH4lGsTAQy" +
-            "DPMF9ZqEDOaL6DJuu0colSBBBQ85hQTPa9m9nyJoN3pEi1hgamqOvQIWcXBk+SOpUGRZZFXwniJV" +
-            "zDKU5nE9MYgn2B9AoiH3CSuMz6HRqgVaqtppIe1jhukMc/kHVJvlKRNy9XMCAwEAATANBgkqhkiG" +
-            "9w0BAQUFAAOBgQC7yBmJ9O/eWDGtSH9BH0R3dh2NdST3W9hNZ8hIa8U8klhNHbUCSSktZmZkvbPU" +
-            "hse5LI3dh6RyNDuqDrbYwcqzKbFJaq/jX9kCoeb3vgbQElMRX8D2ID1vRjxwlALFISrtaN4VpWzV" +
-            "yeoHPW4xldeZmoVtjn8zXNzQhLuBqX2MmAAAAqwAAAAUvkUScfw9yCSmALruURNmtBai7kQAAAZx" +
-            "4Jmijxs/l8EBaleaUru6EOPioWkUAEVWCxjM/TxbGHOi2VMsQWqRr/DZ3wsDmtQgw3QTrUK666sR" +
-            "MBnbqdnyCyvM1J2V1xxLXPUeRBmR2CXorYGF9Dye7NkgVdfA+9g9L/0Au6Ugn+2Cj5leoIgkgApN" +
-            "vuEcZegFlNOUPVEs3SlBgUF1BY6OBM0UBHTPwGGxFBBcetcuMRbUnu65vyDG0pslT59qpaR0TMVs" +
-            "P+tcheEzhyjbfM32/vwhnL9dBEgM8qMt0sqF6itNOQU/F4WGkK2Cm2v4CYEyKYw325fEhzTXosck" +
-            "MhbqmcyLab8EPceWF3dweoUT76+jEZx8lV2dapR+CmczQI43tV9btsd1xiBbBHAKvymm9Ep9bPzM" +
-            "J0MQi+OtURL9Lxke/70/MRueqbPeUlOaGvANTmXQD2OnW7PISwJ9lpeLfTG0LcqkoqkbtLKQLYHI" +
-            "rQfV5j0j+wmvmpMxzjN3uvNajLa4zQ8l0Eok9SFaRr2RL0gN8Q2JegfOL4pUiHPsh64WWya2NB7f" +
-            "V+1s65eA5ospXYsShRjo046QhGTmymwXXzdzuxu8IlnTEont6P4+J+GsWk6cldGbl20hctuUKzyx" +
-            "OptjEPOKejV60iDCYGmHbCWAzQ8h5MILV82IclzNViZmzAapeeCnexhpXhWTs+xDEYSKEiG/camt" +
-            "bhmZc3BcyVJrW23PktSfpBQ6D8ZxoMfF0L7V2GQMaUg+3r7ucrx82kpqotjv0xHghNIm95aBr1Qw" +
-            "1gaEjsC/0wGmmBDg1dTDH+F1p9TInzr3EFuYD0YiQ7YlAHq3cPuyGoLXJ5dXYuSBfhDXJSeddUkl" +
-            "k1ufZyOOcskeInQge7jzaRfmKg3U94r+spMEvb0AzDQVOKvjjo1ivxMSgFRZaDb/4qw=";
-
-        private static final String PASSWORD = "android";
-
-        /**
-         * Loads a keystore from a base64-encoded String. Returns the KeyManager[]
-         * for the result.
-         */
-        private KeyManager[] getKeyManagers() throws Exception {
-            byte[] bytes = Base64.decode(SERVER_KEYS_BKS, Base64.DEFAULT);
-            InputStream inputStream = new ByteArrayInputStream(bytes);
-
-            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
-            keyStore.load(inputStream, PASSWORD.toCharArray());
-            inputStream.close();
-
-            String algorithm = KeyManagerFactory.getDefaultAlgorithm();
-            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
-            keyManagerFactory.init(keyStore, PASSWORD.toCharArray());
-
-            return keyManagerFactory.getKeyManagers();
-        }
-
-
-        public ServerThread(TestWebServer server, boolean ssl) throws Exception {
-            super("ServerThread");
-            mServer = server;
-            mIsSsl = ssl;
-            int retry = 3;
-            while (true) {
-                try {
-                    if (mIsSsl) {
-                        mSslContext = SSLContext.getInstance("TLS");
-                        mSslContext.init(getKeyManagers(), null, null);
-                        mSocket = mSslContext.getServerSocketFactory().createServerSocket(0);
-                    } else {
-                        mSocket = new ServerSocket(0);
-                    }
-                    return;
-                } catch (IOException e) {
-                    Log.w(TAG, e);
-                    if (--retry == 0) {
-                        throw e;
-                    }
-                    // sleep in case server socket is still being closed
-                    Thread.sleep(1000);
-                }
-            }
-        }
-
-        @Override
-        public void run() {
-            HttpParams params = new BasicHttpParams();
-            params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_0);
-            while (!mIsCancelled) {
-                try {
-                    Socket socket = mSocket.accept();
-                    DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
-                    conn.bind(socket, params);
-
-                    // Determine whether we need to shutdown early before
-                    // parsing the response since conn.close() will crash
-                    // for SSL requests due to UnsupportedOperationException.
-                    HttpRequest request = conn.receiveRequestHeader();
-                    if (isShutdownRequest(request)) {
-                        mIsCancelled = true;
-                    }
-
-                    HttpResponse response = mServer.getResponse(request);
-                    conn.sendResponseHeader(response);
-                    conn.sendResponseEntity(response);
-                    conn.close();
-
-                } catch (IOException e) {
-                    // normal during shutdown, ignore
-                    Log.w(TAG, e);
-                } catch (HttpException e) {
-                    Log.w(TAG, e);
-                } catch (InterruptedException e) {
-                    Log.w(TAG, e);
-                } catch (UnsupportedOperationException e) {
-                    // DefaultHttpServerConnection's close() throws an
-                    // UnsupportedOperationException.
-                    Log.w(TAG, e);
-                }
-            }
-            try {
-                mSocket.close();
-            } catch (IOException ignored) {
-                // safe to ignore
-            }
-        }
-
-        private boolean isShutdownRequest(HttpRequest request) {
-            RequestLine requestLine = request.getRequestLine();
-            String uriString = requestLine.getUri();
-            URI uri = URI.create(uriString);
-            String path = uri.getPath();
-            return path.equals(SHUTDOWN_PREFIX);
-        }
-    }
-}
diff --git a/hostsidetests/multidevices/uwb/snippet/Android.bp b/libs/webkit-shared/Android.bp
similarity index 61%
copy from hostsidetests/multidevices/uwb/snippet/Android.bp
copy to libs/webkit-shared/Android.bp
index 2ad7dd2..acd9074 100644
--- a/hostsidetests/multidevices/uwb/snippet/Android.bp
+++ b/libs/webkit-shared/Android.bp
@@ -11,25 +11,28 @@
 // WITHOUT 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 {
-    name: "uwb_snippet",
-    sdk_version: "system_current",
+java_library {
+    name: "ctswebkitsharedenv",
     srcs: [
-        "UwbManagerSnippet.java",
+        "src/**/*.java",
+        "src/**/*.aidl",
     ],
-    manifest: "AndroidManifest.xml",
+    libs: [
+        "android.test.base",
+        "org.apache.http.legacy",
+    ],
     static_libs: [
-        "androidx.test.runner",
-        "guava",
-        "mobly-snippet-lib",
-        "com.uwb.support.ccc",
-        "com.uwb.support.fira",
-        "com.uwb.support.generic",
-        "com.uwb.support.multichip",
+        "compatibility-device-util-axt",
+        "ctsdeviceutillegacy-axt",
+        "ctstestserver",
+        "platform-test-annotations",
+    ],
+    visibility: [
+        "//cts/tests/tests/webkit:__subpackages__",
+        "//cts/tests/tests/sdksandbox/webkit:__subpackages__",
     ],
 }
diff --git a/libs/webkit-shared/OWNERS b/libs/webkit-shared/OWNERS
new file mode 100644
index 0000000..56fc91b
--- /dev/null
+++ b/libs/webkit-shared/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 76427
+bewise@google.com
+include /tests/tests/webkit/OWNERS
\ No newline at end of file
diff --git a/libs/webkit-shared/README.md b/libs/webkit-shared/README.md
new file mode 100644
index 0000000..bea2f78
--- /dev/null
+++ b/libs/webkit-shared/README.md
@@ -0,0 +1,158 @@
+# Shared Webkit Environment
+
+## Overview
+
+This helper lib makes a test suite extendable to run in both an activity based environment and
+within the SDK Runtime.
+
+[design](go/shared-sdk-sandbox-webview-tests) (*only visible to Googlers)
+
+## Expected prior knowledge
+
+Read the test scenario documentation to get a better understanding of how we invoke tests inside
+the SDK Runtime:
+`//packages/modules/AdServices/sdksandbox/tests/testutils/testscenario/README.md`
+
+## Tutorial
+
+*** aside
+**Tip:**  If your test suite already extends SharedWebViewTest, you can skip to section
+3, "Converting a test to shared"
+***
+
+### 1. Making a test suite sharable with the SDK Runtime
+
+If you want to share webkit tests inside the SDK runtime, you will need to make your
+test suite inherit from `android.webkit.cts.SharedWebViewTest`. This is used to indicate
+that a test suite has a configurable environment.
+
+Eg:
+```java
+- public class WebViewTest
++ public class WebViewTest extends SharedWebViewTest
+```
+
+*** aside
+**Note:**  Some WebView tests still use the JUnit 3 style, so you may need to
+first migrate the test suite from `ActivityInstrumentationTestCase2` to use
+`ActivityScenarioRule` (which is for JUnit 4 style). See
+[b/112773416](http://b/112773416) for details.
+***
+
+This abstract class requires you to implement the method `createTestEnvironment` that
+defines the test environment for your test suite. Think of the test environment as a
+concrete description of where this test suite will execute. `createTestEnvironment` should
+have all the references your test has to an activity. This will be overridden later on by the
+SDK tests using the API method `setTestEnvironment`.
+
+Eg:
+```java
+@Override
+protected SharedWebViewTestEnvironment createTestEnvironment() {
+    return new SharedWebViewTestEnvironment.Builder()
+            .setContext(mActivity)
+            .setWebView(mWebView)
+            // This allows SDK methods to invoke APIs from outside the SDK.
+            // The Activity based tests don't need to worry about this so you can
+            // just provide the host app invoker directly to this environment.
+            .setHostAppInvoker(SharedWebViewTestEnvironment.createHostAppInvoker())
+            .build();
+}
+```
+
+Your test suite is now sharable with an SDK runtime test suite!
+
+### 2. Sharing your tests with the SDK Runtime
+
+The SDK Runtime tests for webkit live under `//cts/tests/tests/sdksandbox/webkit`.
+
+You need a test SDK that will actually have your tests, and a JUnit test suite that JUnit will have to invoke your tests from an activity.
+
+You can follow the "Creating new SDK Runtime tests" section under the SDK testscenario
+guide (store these SDK tests in `//cts/tests/tests/sdksandbox/webkit`):
+`//packages/modules/AdServices/sdksandbox/tests/testutils/testscenario/README.md`
+
+*** aside
+**Note:**  If you reuse the WebViewSandboxTestSdk below you will only need to follow the last step of the "Invoke from a JUnit test suite" section from the guide above.
+***
+
+However, instead of creating a new test SDK as per the guide above, you can reuse the WebViewSandboxTestSdk
+`//cts/tests/tests/sdksandbox/webkit/sdksidetests/WebViewSandboxTest/src/com/android/cts/sdksidetests/webviewsandboxtest/WebViewSandboxTestSdk.java`
+To do this use the `android.sdksandbox.webkit.cts.WebViewSandboxTestRule` and pass in the fully qualified name of your test class.
+
+Congratulations! Your webkit tests are now shared with your SDK Runtime tests!
+
+### 3. Converting a test to shared
+
+You need to do two things when you are making a test shared:
+1. Update the test suite to use the `SharedWebViewTestEnvironment`
+2. Update the SDK JUnit Test Suite to invoke the test
+
+We will use `WebViewTest` as an example:
+`//cts/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java`
+
+Search for `getTestEnvironment()`. This method returns a `SharedWebViewTestEnvironment`.
+Whenever your test needs to refer to anything that is not available in the SDK runtime,
+or needs to be shared between the SDK runtime and the activity based tests,
+use this class.
+
+Open `SharedWebViewTestEnvironment` to familiarize yourself with what is available:
+`//cts/libs/webkit-shared/src/android/webkit/cts/SharedWebViewTestEnvironment.java`
+
+First convert any direct references to any variable that should come from the shared test
+environment.
+
+Eg:
+```java
+@Test
+public void testExample() throws Throwable {
+    - InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+    + getTestEnvironment().waitForIdleSync();
+    ...
+}
+```
+
+*** note
+**Tip:**  You can likely just update your setup method to pull from the test environment for
+minimal refactoring. Eg:
+
+```java
+@Test
+public void setup() {
+    mWebView = getTestEnvironment().getWebView();
+    ...
+}
+```
+***
+
+Next you will invoke this shared test from your SDK JUnit test suite. An example of a JUnit
+test suite can be found here:
+`//cts/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/WebViewSandboxTest.java`
+
+You can see in this file that we use `SdkSandboxScenarioRule#assertSdkTestRunPasses` to invoke
+test methods.
+
+And that's it! Your test should now run! You can test that your method was added with `atest`:
+
+```sh
+# Confirm the test runs in the sandbox
+atest CtsSdkSandboxWebkitTestCases:WebViewSandboxTest#<shared_test>
+# Confirm the test still runs normally
+atest CtsWebkitTestCases:WebViewTest#<shared_test>
+```
+
+## Invoking behavior in the Activity
+
+There are some things you won't be able to initiate from within the SDK runtime
+that are needed to write tests. For example, you cannot start a local server
+in the SDK runtime, but this would be useful for testing against.
+
+You can use the
+ActivityInvoker (`//cts/libs/webkit-shared/src/android/webkit/cts/IActivityInvoker.aidl`)
+to add this functionality.
+The activity invoker allows SDK runtime tests to initiate events in the activity driving
+the tests.
+
+Once you have added a new ActivityInvoker API, provide a wrapper API to SharedWebViewTestEnvironment
+to abstract these APIs away from test authors.
diff --git a/libs/webkit-shared/src/android/webkit/cts/ExceptionWrapper.java b/libs/webkit-shared/src/android/webkit/cts/ExceptionWrapper.java
new file mode 100644
index 0000000..a4aca08
--- /dev/null
+++ b/libs/webkit-shared/src/android/webkit/cts/ExceptionWrapper.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.webkit.cts;
+
+import android.os.RemoteException;
+
+/**
+ * Binder only handles a few exceptions. Runtime exceptions are silently ignored and any errors
+ * thrown will result in crashing the entire process and meaning no further tests will pass. We deal
+ * with this in a bit of a sneaky sneaky way and wrap any throwables in one of the supported
+ * exception types (IllegalStateException seems fairly representative of if the host app environment
+ * is broken), and then catch that and re-expose it in the SharedSdkWebServer where we strip that
+ * exception type out to avoid confusion.
+ *
+ * <p>The wrap/unwrap methods from this class should be used on either side of IPC calls by the
+ * IHostAppInvoker and IWebServer.
+ *
+ * <p>This allows JUnit to deal with any major broken program flows gracefully instead of moving on
+ * or crashing the rest of the tests.
+ *
+ * <p>It should be noted that binder parcel will take the cause and stringify the stack trace so the
+ * type information of these exceptions is lost in the journey. This means that the test code will
+ * not be able to react to these exception types. This is already a limitation of communicating
+ * through binder.
+ */
+class ExceptionWrapper {
+    public static <T> T wrap(WrappedTypedCall<T> c) {
+        try {
+            return c.wrap();
+        } catch (Throwable e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    public static void wrap(WrappedVoidCall r) {
+        wrap(() -> {
+            r.wrap();
+            return null;
+        });
+    }
+
+    public static <T> T unwrap(UnwrappedTypedCall<T> c) {
+        try {
+            return c.unwrap();
+        } catch (RemoteException e) {
+            // We are handling the remote exception separately from the IllegalStateException
+            // because this is happening binder proxy side so we would like to preserve the
+            // exception information.
+            // We still wrap this in a runtime exception so that the WebServer tests don't need to
+            // throw inside the Webkit utils run on main sync method as that would mean those
+            // functions would all have to return null (it would turn them into callables instead of
+            // runnables).
+            throw new RuntimeException(e);
+        } catch (IllegalStateException e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    public static void unwrap(UnwrappedVoidCall c) {
+        unwrap(() -> {
+            c.unwrap();
+            return null;
+        });
+    }
+
+    interface WrappedTypedCall<T> {
+        T wrap() throws Exception;
+    }
+
+    interface WrappedVoidCall {
+        void wrap() throws Exception;
+    }
+
+    interface UnwrappedTypedCall<T> {
+        T unwrap() throws RemoteException;
+    }
+
+    interface UnwrappedVoidCall {
+        void unwrap() throws RemoteException;
+    }
+}
diff --git a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt b/libs/webkit-shared/src/android/webkit/cts/HttpHeader.aidl
similarity index 60%
copy from tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
copy to libs/webkit-shared/src/android/webkit/cts/HttpHeader.aidl
index 101f200..644be02 100644
--- a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
+++ b/libs/webkit-shared/src/android/webkit/cts/HttpHeader.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,16 +14,6 @@
  * limitations under the License.
  */
 
-package android.os.cts.autorevokeqapp
+package android.webkit.cts;
 
-import android.app.Activity
-import android.os.Bundle
-
-class MainActivity : Activity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        requestPermissions(arrayOf("android.permission.READ_CALENDAR"), 0)
-    }
-}
+parcelable HttpHeader;
diff --git a/libs/webkit-shared/src/android/webkit/cts/HttpHeader.java b/libs/webkit-shared/src/android/webkit/cts/HttpHeader.java
new file mode 100644
index 0000000..26e0154
--- /dev/null
+++ b/libs/webkit-shared/src/android/webkit/cts/HttpHeader.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.webkit.cts;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class attempts to match the Apache HttpRequest as close as possible so that it can be used
+ * in place of it. The apache HttpRequest is not parcelable.
+ */
+public class HttpHeader implements Parcelable {
+    private final String mHeader;
+    private final String mValue;
+
+    public static final Parcelable.Creator<HttpHeader> CREATOR =
+            new Parcelable.Creator<HttpHeader>() {
+                public HttpHeader createFromParcel(Parcel in) {
+                    return new HttpHeader(in);
+                }
+
+                public HttpHeader[] newArray(int size) {
+                    return new HttpHeader[size];
+                }
+            };
+
+    /** Create a new HttpHeader from a header name and value string. */
+    public static HttpHeader create(String header, String value) {
+        return new HttpHeader(header, value);
+    }
+
+    /** Convert a list of HttpHeaders to a List of pairs to be used with CtsTestServer. */
+    public static List<Pair<String, String>> asPairList(List<HttpHeader> headers) {
+        List<Pair<String, String>> pairList = new ArrayList<>();
+        if (headers != null) {
+            for (HttpHeader header : headers) {
+                pairList.add(header.getPair());
+            }
+        }
+        return pairList;
+    }
+
+    HttpHeader(Parcel in) {
+        // Note: This must be read in the same order we write
+        // to the parcel in {@link #wroteToParcel(Parcel out, int flags)}.
+        this(in.readString(), in.readString());
+    }
+
+    HttpHeader(String header, String value) {
+        mHeader = header;
+        mValue = value;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        // Note: This must be written in the same order we read
+        // from the parcel in {@link #HttpRequest(Parcel in)}.
+        out.writeString(mHeader);
+        out.writeString(mValue);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    private Pair<String, String> getPair() {
+        return Pair.create(mHeader, mValue);
+    }
+}
diff --git a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt b/libs/webkit-shared/src/android/webkit/cts/HttpRequest.aidl
similarity index 60%
copy from tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
copy to libs/webkit-shared/src/android/webkit/cts/HttpRequest.aidl
index 101f200..3ed264b 100644
--- a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
+++ b/libs/webkit-shared/src/android/webkit/cts/HttpRequest.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,16 +14,6 @@
  * limitations under the License.
  */
 
-package android.os.cts.autorevokeqapp
+package android.webkit.cts;
 
-import android.app.Activity
-import android.os.Bundle
-
-class MainActivity : Activity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        requestPermissions(arrayOf("android.permission.READ_CALENDAR"), 0)
-    }
-}
+parcelable HttpRequest;
diff --git a/libs/webkit-shared/src/android/webkit/cts/HttpRequest.java b/libs/webkit-shared/src/android/webkit/cts/HttpRequest.java
new file mode 100644
index 0000000..56b35ce
--- /dev/null
+++ b/libs/webkit-shared/src/android/webkit/cts/HttpRequest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.webkit.cts;
+
+import static org.junit.Assert.fail;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * This class attempts to match the Apache HttpRequest as close as possible so that it can be used
+ * in place of it. The apache HttpRequest is not parcelable.
+ */
+public class HttpRequest implements Parcelable {
+    private final Locale mLowerCaseLocale = Locale.forLanguageTag("en-US");
+    private Bundle mHeaders;
+    private String mUrl;
+    private String mMethod;
+    private String mBody;
+
+    public static final Parcelable.Creator<HttpRequest> CREATOR =
+            new Parcelable.Creator<HttpRequest>() {
+                public HttpRequest createFromParcel(Parcel in) {
+                    return new HttpRequest(in);
+                }
+
+                public HttpRequest[] newArray(int size) {
+                    return new HttpRequest[size];
+                }
+            };
+
+    public HttpRequest(String url, org.apache.http.HttpRequest apacheRequest) {
+        mHeaders = new Bundle();
+        mUrl = url;
+        mMethod = apacheRequest.getRequestLine().getMethod();
+        mBody = null;
+
+        try {
+            if (apacheRequest instanceof HttpEntityEnclosingRequest) {
+                HttpEntity entity = ((HttpEntityEnclosingRequest) apacheRequest).getEntity();
+                mBody = EntityUtils.toString(entity);
+            }
+        } catch (IOException err) {
+            fail("Failed to request request body");
+        }
+
+        for (Header header : apacheRequest.getAllHeaders()) {
+            ArrayList<String> headerValues = mHeaders.getStringArrayList(header.getName());
+            if (headerValues == null) {
+                headerValues = new ArrayList<String>();
+            }
+
+            headerValues.add(header.getValue());
+            // Http Headers are case insensitive so storing all headers as lowercase.
+            // Lowercasing by locale to avoid the infamous turkish locale bug.
+            mHeaders.putStringArrayList(
+                    header.getName().toLowerCase(mLowerCaseLocale), headerValues);
+        }
+    }
+
+    HttpRequest(Parcel in) {
+        // Note: This must be read in the same order we write
+        // to the parcel in {@link #wroteToParcel(Parcel out, int flags)}.
+        mHeaders = in.readBundle();
+        mUrl = in.readString();
+        mMethod = in.readString();
+        mBody = in.readString();
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        // Note: This must be written in the same order we read
+        // from the parcel in {@link #HttpRequest(Parcel in)}.
+        out.writeBundle(mHeaders);
+        out.writeString(mUrl);
+        out.writeString(mMethod);
+        out.writeString(mBody);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Retrieve all the headers for the URL request matching a header string. */
+    public String[] getHeaders(String header) {
+        ArrayList<String> headers =
+                mHeaders.getStringArrayList(header.toLowerCase(mLowerCaseLocale));
+        if (headers != null) {
+            return headers.toArray(new String[] {});
+        }
+        return new String[] {};
+    }
+
+    /** Returns the url of the request. */
+    public String getUrl() {
+        return mUrl;
+    }
+
+    /** Returns the method of the request. */
+    public String getMethod() {
+        return mMethod;
+    }
+
+    /** Returns the body of the request. */
+    public String getBody() {
+        return mBody;
+    }
+}
diff --git a/libs/webkit-shared/src/android/webkit/cts/IHostAppInvoker.aidl b/libs/webkit-shared/src/android/webkit/cts/IHostAppInvoker.aidl
new file mode 100644
index 0000000..ddc687c
--- /dev/null
+++ b/libs/webkit-shared/src/android/webkit/cts/IHostAppInvoker.aidl
@@ -0,0 +1,37 @@
+/*
+ * 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.webkit.cts;
+
+import android.view.MotionEvent;
+
+import android.webkit.cts.IWebServer;
+
+/**
+ * This shared interface is used to invoke methods
+ * that belong to the activity of a test.
+ */
+interface IHostAppInvoker {
+    void waitForIdleSync();
+
+    void sendKeyDownUpSync(int keyCode);
+
+    void sendPointerSync(in MotionEvent event);
+
+    byte[] getEncodingBytes(String data, String charset);
+
+    IWebServer getWebServer();
+}
diff --git a/libs/webkit-shared/src/android/webkit/cts/IWebServer.aidl b/libs/webkit-shared/src/android/webkit/cts/IWebServer.aidl
new file mode 100644
index 0000000..77cde38
--- /dev/null
+++ b/libs/webkit-shared/src/android/webkit/cts/IWebServer.aidl
@@ -0,0 +1,65 @@
+/*
+ * 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.webkit.cts;
+
+import android.webkit.cts.HttpRequest;
+import android.webkit.cts.HttpHeader;
+
+import java.util.List;
+
+interface IWebServer {
+    void start(int sslMode, in @nullable byte[] acceptedIssuerDer, int keyResId, int certResId);
+
+    void shutdown();
+
+    void resetRequestState();
+
+    String setResponse(
+        String path, String responseString, in List<HttpHeader> responseHeaders);
+
+    String getAbsoluteUrl(String path);
+
+    String getUserAgentUrl();
+
+    String getDelayedAssetUrl(String path);
+
+    String getRedirectingAssetUrl(String path);
+
+    String getAssetUrl(String path);
+
+    String getAuthAssetUrl(String path);
+
+    String getBinaryUrl(String mimeType, int contentLength);
+
+    String getAppCacheUrl();
+
+    int getRequestCount();
+
+    int getRequestCountWithPath(String path);
+
+    boolean wasResourceRequested(String url);
+
+    HttpRequest getLastRequest(String path);
+
+    HttpRequest getLastAssetRequest(String url);
+
+    String getCookieUrl(String path);
+
+    String getSetCookieUrl(String path, String key, String value, String attributes);
+
+    String getLinkedScriptUrl(String path, String url);
+}
\ No newline at end of file
diff --git a/libs/webkit-shared/src/android/webkit/cts/SharedSdkWebServer.java b/libs/webkit-shared/src/android/webkit/cts/SharedSdkWebServer.java
new file mode 100644
index 0000000..ac3fb6d
--- /dev/null
+++ b/libs/webkit-shared/src/android/webkit/cts/SharedSdkWebServer.java
@@ -0,0 +1,191 @@
+/*
+ * 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.webkit.cts;
+
+
+import androidx.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class serves as the public fronting API for tests to interact with the CtsTestServer.
+ *
+ * <p>This is a light wrapper around the Binder Proxy so that we can wrap all the RemoteExceptions
+ * in a Runtime exception so that the WebServer methods can be used the same as they were used
+ * previously.
+ */
+public final class SharedSdkWebServer {
+    private final IWebServer mWebServer;
+
+    public SharedSdkWebServer(IWebServer webServer) {
+        mWebServer = webServer;
+    }
+
+    /** Starts the web server using the provided parameters}. */
+    public void start(
+            @SslMode int sslMode, @Nullable byte[] acceptedIssuerDer, int keyResId, int certResId) {
+        ExceptionWrapper.unwrap(() -> {
+            mWebServer.start(sslMode, acceptedIssuerDer, keyResId, certResId);
+        });
+    }
+
+    /** Shuts down the web server if it was started. */
+    public void shutdown() {
+        ExceptionWrapper.unwrap(() -> {
+            mWebServer.shutdown();
+        });
+    }
+
+    /** Resets all request state stored. */
+    public void resetRequestState() {
+        ExceptionWrapper.unwrap(() -> {
+            mWebServer.resetRequestState();
+        });
+    }
+
+    /**
+     * Sets a response to be returned when a particular request path is passed in (with the option
+     * to specify additional headers).
+     */
+    public String setResponse(
+            String path, String responseString, List<HttpHeader> responseHeaders) {
+        return ExceptionWrapper.unwrap(() -> {
+            // We can't send a null value as a list
+            // so default the responseHeaders to an empty list if null was provided.
+            return mWebServer.setResponse(
+                    path,
+                    responseString,
+                    responseHeaders == null ? Collections.emptyList() : responseHeaders);
+        });
+    }
+
+    /** Return the absolute URL that refers to a path. */
+    public String getAbsoluteUrl(String path) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getAbsoluteUrl(path);
+        });
+    }
+
+    /** Returns a url that will contain the user agent in the header and in the body. */
+    public String getUserAgentUrl() {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getUserAgentUrl();
+        });
+    }
+
+    /** Get a delayed assert url for an asset path. */
+    public String getDelayedAssetUrl(String path) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getDelayedAssetUrl(path);
+        });
+    }
+
+    /** Get a url that will redirect for a path. */
+    public String getRedirectingAssetUrl(String path) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getRedirectingAssetUrl(path);
+        });
+    }
+
+    /** Get the full url for an asset. */
+    public String getAssetUrl(String path) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getAssetUrl(path);
+        });
+    }
+
+    /** Get the full auth url for an asset. */
+    public String getAuthAssetUrl(String path) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getAuthAssetUrl(path);
+        });
+    }
+
+    /** Get a binary url. */
+    public String getBinaryUrl(String mimeType, int contentLength) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getBinaryUrl(mimeType, contentLength);
+        });
+    }
+
+    /** Returns the url to the app cache. */
+    public String getAppCacheUrl() {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getAppCacheUrl();
+        });
+    }
+
+    /** Returns how many requests have been made. */
+    public int getRequestCount() {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getRequestCount();
+        });
+    }
+
+    /** Returns the request count for a particular path */
+    public int getRequestCount(String path) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getRequestCountWithPath(path);
+        });
+    }
+
+    /** Verify if a resource was requested. */
+    public boolean wasResourceRequested(String url) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.wasResourceRequested(url);
+        });
+    }
+
+    /** Retrieve the last request to be made on a url. */
+    public HttpRequest getLastRequest(String path) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getLastRequest(path);
+        });
+    }
+
+    /** Retrieve the last request for an asset path to be made on a url. */
+    public HttpRequest getLastAssetRequest(String url) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getLastAssetRequest(url);
+        });
+    }
+
+    /** Returns a url that will contain the path as a cookie. */
+    public String getCookieUrl(String path) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getCookieUrl(path);
+        });
+    }
+
+    /**
+     * Returns a URL that attempts to set the cookie "key=value" with the given list of attributes
+     * when fetched.
+     */
+    public String getSetCookieUrl(String path, String key, String value, String attributes) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getSetCookieUrl(path, key, value, attributes);
+        });
+    }
+
+    /** Returns a URL for a page with a script tag where src equals the URL passed in. */
+    public String getLinkedScriptUrl(String path, String url) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mWebServer.getLinkedScriptUrl(path, url);
+        });
+    }
+}
diff --git a/libs/webkit-shared/src/android/webkit/cts/SharedWebViewTest.java b/libs/webkit-shared/src/android/webkit/cts/SharedWebViewTest.java
new file mode 100644
index 0000000..3265e26
--- /dev/null
+++ b/libs/webkit-shared/src/android/webkit/cts/SharedWebViewTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.webkit.cts;
+
+/**
+ * Extending this class indicates that a test can be shared between the SDK Runtime and Activity
+ * based tests.
+ *
+ * <p>If a test is shared, it will be expected to provide its own {@link
+ * SharedWebViewTestEnvironment} implementation.
+ */
+public abstract class SharedWebViewTest {
+    public static final String WEB_VIEW_TEST_CLASS_NAME = "WEB_VIEW_TEST_CLASS_NAME";
+
+    private SharedWebViewTestEnvironment mEnvironment;
+
+    protected abstract SharedWebViewTestEnvironment createTestEnvironment();
+
+    public void setTestEnvironment(SharedWebViewTestEnvironment sharedWebViewTestEnvironment) {
+        mEnvironment = sharedWebViewTestEnvironment;
+    }
+
+    protected SharedWebViewTestEnvironment getTestEnvironment() {
+        if (mEnvironment == null) {
+            mEnvironment = createTestEnvironment();
+        }
+        return mEnvironment;
+    }
+}
diff --git a/libs/webkit-shared/src/android/webkit/cts/SharedWebViewTestEnvironment.java b/libs/webkit-shared/src/android/webkit/cts/SharedWebViewTestEnvironment.java
new file mode 100644
index 0000000..d777c62
--- /dev/null
+++ b/libs/webkit-shared/src/android/webkit/cts/SharedWebViewTestEnvironment.java
@@ -0,0 +1,449 @@
+/*
+ * 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.webkit.cts;
+
+import static org.junit.Assert.*;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.StrictMode;
+import android.os.StrictMode.ThreadPolicy;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+
+import org.apache.http.util.EncodingUtils;
+
+import java.io.ByteArrayInputStream;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * This class contains all the environmental variables that need to be configured for WebView tests
+ * to either run inside the SDK Runtime or within an Activity.
+ */
+public final class SharedWebViewTestEnvironment {
+    @Nullable private final Context mContext;
+    @Nullable private final WebView mWebView;
+    @Nullable private final FrameLayout mRootLayout;
+    private final IHostAppInvoker mHostAppInvoker;
+
+    private SharedWebViewTestEnvironment(
+            Context context,
+            WebView webView,
+            IHostAppInvoker hostAppInvoker,
+            FrameLayout rootLayout) {
+        mContext = context;
+        mWebView = webView;
+        mHostAppInvoker = hostAppInvoker;
+        mRootLayout = rootLayout;
+    }
+
+    @Nullable
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Nullable
+    public WebView getWebView() {
+        return mWebView;
+    }
+
+    /**
+     * Some tests require adding a content view to the root view at runtime. This method mimics the
+     * behaviour of Activity.addContentView()
+     */
+    @Nullable
+    public void addContentView(View view, ViewGroup.LayoutParams params) {
+        view.setLayoutParams(params);
+        mRootLayout.addView(view);
+    }
+
+    /**
+     * Apache Utils can't be statically linked so we can't use them directly inside the SDK Runtime.
+     * Use this method instead of EncodingUtils.getBytes.
+     */
+    public byte[] getEncodingBytes(String data, String charset) {
+        return ExceptionWrapper.unwrap(() -> {
+            return mHostAppInvoker.getEncodingBytes(data, charset);
+        });
+    }
+
+    /** Invokes waitForIdleSync on the {@link Instrumentation} in the activity. */
+    public void waitForIdleSync() {
+        ExceptionWrapper.unwrap(() -> {
+            mHostAppInvoker.waitForIdleSync();
+        });
+    }
+
+    /** Invokes sendKeyDownUpSync on the {@link Instrumentation} in the activity. */
+    public void sendKeyDownUpSync(int keyCode) {
+        ExceptionWrapper.unwrap(() -> {
+            mHostAppInvoker.sendKeyDownUpSync(keyCode);
+        });
+    }
+
+    /** Invokes sendPointerSync on the {@link Instrumentation} in the activity. */
+    public void sendPointerSync(MotionEvent event) {
+        ExceptionWrapper.unwrap(() -> {
+            mHostAppInvoker.sendPointerSync(event);
+        });
+    }
+
+    /** Returns a web server that can be used for web based testing. */
+    public SharedSdkWebServer getWebServer() {
+        return ExceptionWrapper.unwrap(() -> {
+            return new SharedSdkWebServer(mHostAppInvoker.getWebServer());
+        });
+    }
+
+    /** Returns a web server that has been started and can be used for web based testing. */
+    public SharedSdkWebServer getSetupWebServer(@SslMode int sslMode) {
+        return getSetupWebServer(sslMode, null, 0, 0);
+    }
+
+    /** Returns a web server that has been started and can be used for web based testing. */
+    public SharedSdkWebServer getSetupWebServer(
+            @SslMode int sslMode, @Nullable byte[] acceptedIssuerDer, int keyResId, int certResId) {
+        SharedSdkWebServer webServer = getWebServer();
+        webServer.start(sslMode, acceptedIssuerDer, keyResId, certResId);
+        return webServer;
+    }
+
+    /**
+     * Use this builder to create a {@link SharedWebViewTestEnvironment}. The {@link
+     * SharedWebViewTestEnvironment} can not be built directly.
+     */
+    public static final class Builder {
+        private Context mContext;
+        private WebView mWebView;
+
+        private FrameLayout mRootLayout;
+        private IHostAppInvoker mHostAppInvoker;
+
+        /** Provide a {@link Context} the tests should use for your environment. */
+        public Builder setContext(@NonNull Context context) {
+            mContext = context;
+            return this;
+        }
+
+        /** Provide a {@link WebView} the tests should use for your environment. */
+        public Builder setWebView(@NonNull WebView webView) {
+            mWebView = webView;
+            return this;
+        }
+
+        /**
+         * Provide a {@link IHostAppInvoker} the tests should use for your environment.
+         *
+         * <p>This can be created with {@link createHostAppInvoker}.
+         *
+         * <p>Note: This is required.
+         */
+        public Builder setHostAppInvoker(@NonNull IHostAppInvoker hostAppInvoker) {
+            mHostAppInvoker = hostAppInvoker;
+            return this;
+        }
+
+        /** Provide a {@link FrameLayout} the tests should use for your environment. */
+        public Builder setRootLayout(@NonNull FrameLayout rootLayout) {
+            mRootLayout = rootLayout;
+            return this;
+        }
+
+        /** Build a new SharedWebViewTestEnvironment. */
+        public SharedWebViewTestEnvironment build() {
+            if (mHostAppInvoker == null) {
+                throw new NullPointerException("The host app invoker is required");
+            }
+            return new SharedWebViewTestEnvironment(
+                    mContext, mWebView, mHostAppInvoker, mRootLayout);
+        }
+    }
+
+    /**
+     * UiAutomation sends events at device level which lets us get around issues with sending
+     * instrumented events to the SDK Runtime but we don't want this for the regular tests. If
+     * something like a dialog pops up while an input event is being sent, the instrumentation would
+     * treat that as an issue while the UiAutomation input event would just send it through.
+     *
+     * <p>So by default, we disable this use and only use it in the SDK Sandbox.
+     *
+     * <p>This API is used for regular activity based tests.
+     */
+    public static IHostAppInvoker.Stub createHostAppInvoker(Context applicationContext) {
+        return createHostAppInvoker(applicationContext, false);
+    }
+    /**
+     * This will generate a new {@link IHostAppInvoker} binder node. This should be called from
+     * wherever the activity exists for test cases.
+     */
+    public static IHostAppInvoker.Stub createHostAppInvoker(
+            Context applicationContext, boolean allowUiAutomation) {
+        return new IHostAppInvoker.Stub() {
+            private Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+            private UiAutomation mUiAutomation;
+
+            public void waitForIdleSync() {
+                ExceptionWrapper.wrap(() -> {
+                    mInstrumentation.waitForIdleSync();
+                });
+            }
+
+            public void sendKeyDownUpSync(int keyCode) {
+                ExceptionWrapper.wrap(() -> {
+                    mInstrumentation.sendKeyDownUpSync(keyCode);
+                });
+            }
+
+            public void sendPointerSync(MotionEvent event) {
+                ExceptionWrapper.wrap(() -> {
+                    if (allowUiAutomation) {
+                        sendPointerSyncWithUiAutomation(event);
+                    } else {
+                        sendPointerSyncWithInstrumentation(event);
+                    }
+                });
+            }
+
+            public byte[] getEncodingBytes(String data, String charset) {
+                return ExceptionWrapper.wrap(() -> {
+                    return EncodingUtils.getBytes(data, charset);
+                });
+            }
+
+            public IWebServer getWebServer() {
+                return new IWebServer.Stub() {
+                    private CtsTestServer mWebServer;
+
+                    public void start(
+                            @SslMode int sslMode,
+                            @Nullable byte[] acceptedIssuerDer,
+                            int keyResId,
+                            int certResId) {
+                        ExceptionWrapper.wrap(() -> {
+                            assertNull(mWebServer);
+                            final X509Certificate[] acceptedIssuerCerts;
+                            if (acceptedIssuerDer != null) {
+                                CertificateFactory certFactory =
+                                        CertificateFactory.getInstance("X.509");
+                                acceptedIssuerCerts = new X509Certificate[] {
+                                    (X509Certificate) certFactory.generateCertificate(
+                                            new ByteArrayInputStream(acceptedIssuerDer))
+                                };
+                            } else {
+                                acceptedIssuerCerts = null;
+                            }
+                            X509TrustManager trustManager =
+                                    new CtsTestServer.CtsTrustManager() {
+                                        @Override
+                                        public X509Certificate[] getAcceptedIssuers() {
+                                            return acceptedIssuerCerts;
+                                        }
+                                    };
+                            mWebServer = new CtsTestServer(
+                                    applicationContext, sslMode, trustManager, keyResId, certResId);
+                        });
+                    }
+
+                    public void shutdown() {
+                        if (mWebServer == null) {
+                            return;
+                        }
+                        ExceptionWrapper.wrap(() -> {
+                            ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
+                            ThreadPolicy tmpPolicy =
+                                    new ThreadPolicy.Builder(oldPolicy)
+                                            .permitNetwork()
+                                            .build();
+                            StrictMode.setThreadPolicy(tmpPolicy);
+                            mWebServer.shutdown();
+                            mWebServer = null;
+                            StrictMode.setThreadPolicy(oldPolicy);
+                        });
+                    }
+
+                    public void resetRequestState() {
+                        ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            mWebServer.resetRequestState();
+                        });
+                    }
+
+                    public String setResponse(
+                            String path, String responseString, List<HttpHeader> responseHeaders) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.setResponse(
+                                    path, responseString, HttpHeader.asPairList(responseHeaders));
+                        });
+                    }
+
+                    public String getAbsoluteUrl(String path) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getAbsoluteUrl(path);
+                        });
+                    }
+
+                    public String getUserAgentUrl() {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getUserAgentUrl();
+                        });
+                    }
+
+                    public String getDelayedAssetUrl(String path) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getDelayedAssetUrl(path);
+                        });
+                    }
+
+                    public String getRedirectingAssetUrl(String path) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getRedirectingAssetUrl(path);
+                        });
+                    }
+
+                    public String getAssetUrl(String path) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getAssetUrl(path);
+                        });
+                    }
+
+                    public String getAuthAssetUrl(String path) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getAuthAssetUrl(path);
+                        });
+                    }
+
+                    public String getBinaryUrl(String mimeType, int contentLength) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getBinaryUrl(mimeType, contentLength);
+                        });
+                    }
+
+                    public String getAppCacheUrl() {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getAppCacheUrl();
+                        });
+                    }
+
+                    public int getRequestCount() {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getRequestCount();
+                        });
+                    }
+
+                    public int getRequestCountWithPath(String path) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getRequestCount(path);
+                        });
+                    }
+
+                    public boolean wasResourceRequested(String url) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.wasResourceRequested(url);
+                        });
+                    }
+
+                    public HttpRequest getLastRequest(String path) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return toHttpRequest(path, mWebServer.getLastRequest(path));
+                        });
+                    }
+
+                    public HttpRequest getLastAssetRequest(String url) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return toHttpRequest(url, mWebServer.getLastAssetRequest(url));
+                        });
+                    }
+
+                    public String getCookieUrl(String path) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getCookieUrl(path);
+                        });
+                    }
+
+                    public String getSetCookieUrl(
+                            String path, String key, String value, String attributes) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getSetCookieUrl(path, key, value, attributes);
+                        });
+                    }
+
+                    public String getLinkedScriptUrl(String path, String url) {
+                        return ExceptionWrapper.wrap(() -> {
+                            assertNotNull("The WebServer needs to be started", mWebServer);
+                            return mWebServer.getLinkedScriptUrl(path, url);
+                        });
+                    }
+
+                    private HttpRequest toHttpRequest(
+                            String url, org.apache.http.HttpRequest apacheRequest) {
+                        if (apacheRequest == null) {
+                            return null;
+                        }
+
+                        return new HttpRequest(url, apacheRequest);
+                    }
+                };
+            }
+
+            private void sendPointerSyncWithInstrumentation(MotionEvent event) {
+                mInstrumentation.sendPointerSync(event);
+            }
+
+            private void sendPointerSyncWithUiAutomation(MotionEvent event) {
+                if (mUiAutomation == null) {
+                    mUiAutomation = mInstrumentation.getUiAutomation();
+
+                    if (mUiAutomation == null) {
+                        fail("Could not retrieve UI automation");
+                    }
+                }
+
+                if (!mUiAutomation.injectInputEvent(event, true)) {
+                    fail("Could not inject motion event");
+                }
+            }
+        };
+    }
+}
diff --git a/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestUtils.java b/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestUtils.java
index cf105c2..5564c6e 100644
--- a/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestUtils.java
+++ b/tests/MediaProviderTranscode/src/android/mediaprovidertranscode/cts/TranscodeTestUtils.java
@@ -16,17 +16,16 @@
 
 package android.mediaprovidertranscode.cts;
 
-import static androidx.test.InstrumentationRegistry.getContext;
-
 import static android.mediaprovidertranscode.cts.TranscodeTestConstants.INTENT_EXTRA_CALLING_PKG;
 import static android.mediaprovidertranscode.cts.TranscodeTestConstants.INTENT_EXTRA_PATH;
-import static android.mediaprovidertranscode.cts.TranscodeTestConstants.OPEN_FILE_QUERY;
 import static android.mediaprovidertranscode.cts.TranscodeTestConstants.INTENT_QUERY_TYPE;
 
+import static androidx.test.InstrumentationRegistry.getContext;
+
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.Manifest;
 import android.app.ActivityManager;
@@ -38,6 +37,10 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecList;
+import android.media.MediaFormat;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
@@ -46,18 +49,12 @@
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.storage.StorageManager;
-import android.os.storage.StorageVolume;
 import android.provider.MediaStore;
 import android.system.Os;
 import android.system.OsConstants;
 import android.util.Log;
 
-import android.media.MediaCodecInfo;
-import android.media.MediaCodecInfo.CodecCapabilities;
-import android.media.MediaCodecInfo.VideoCapabilities;
-import android.media.MediaCodecList;
-import android.media.MediaFormat;
-
+import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
 
 import com.android.cts.install.lib.Install;
@@ -85,6 +82,8 @@
 
     private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20);
     private static final long POLLING_SLEEP_MILLIS = 100;
+    private static final String TRANSCODE_COMPAT_MANIFEST_DEVICE_CONFIG_PROPERTY_NAME =
+            "transcode_compat_manifest";
 
     public static Uri stageHEVCVideoFile(File videoFile) throws IOException {
         return stageVideoFile(videoFile, R.raw.testvideo_HEVC);
@@ -165,7 +164,7 @@
         assertThat(numBytesWritten).isEqualTo(byteCount);
     }
 
-    public static void enableTranscodingForPackage(String packageName) throws Exception {
+    public static void enableTranscodingForPackage(String packageName) throws IOException {
         executeShellCommand("device_config put storage_native_boot transcode_compat_manifest "
                 + packageName + ",0");
         SystemClock.sleep(1000);
@@ -486,4 +485,9 @@
         }
         return false;
     }
+
+    @NonNull
+    private static UiAutomation getUiAutomation() {
+        return InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    }
 }
diff --git a/tests/PhotoPicker/AndroidManifest.xml b/tests/PhotoPicker/AndroidManifest.xml
index e10bf52..ca87690 100644
--- a/tests/PhotoPicker/AndroidManifest.xml
+++ b/tests/PhotoPicker/AndroidManifest.xml
@@ -19,7 +19,21 @@
           package="android.photopicker.cts">
 <application android:label="Photo Picker Device Tests">
     <uses-library android:name="android.test.runner" />
-    <activity android:name="android.photopicker.cts.GetResultActivity" />
+    <activity android:name="android.photopicker.cts.GetResultActivity"
+              android:exported="false"
+              android:enabled="true">
+        <intent-filter android:priority="999">
+            <action android:name="android.provider.action.PICK_IMAGES" />
+            <category android:name="android.intent.category.DEFAULT" />
+            <data android:mimeType="image/*" />
+            <data android:mimeType="video/*" />
+        </intent-filter>
+        <intent-filter android:priority="999">
+            <action android:name="android.provider.action.PICK_IMAGES" />
+            <category android:name="android.intent.category.DEFAULT" />
+        </intent-filter>
+    </activity>
+
 
     <provider android:name="android.photopicker.cts.cloudproviders.CloudProviderPrimary"
               android:authorities="android.photopicker.cts.cloudproviders.cloud_primary"
diff --git a/tests/PhotoPicker/TEST_MAPPING b/tests/PhotoPicker/TEST_MAPPING
index f48e90c..2a55a28 100644
--- a/tests/PhotoPicker/TEST_MAPPING
+++ b/tests/PhotoPicker/TEST_MAPPING
@@ -1,6 +1,26 @@
 {
+  "mainline-presubmit": [
+    {
+      "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        }
+      ]
+    }
+  ],
   "presubmit": [
     {
+      "name": "CtsPhotoPickerTest",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.LargeTest"
+        }
+      ]
+    }
+  ],
+  "postsubmit": [
+    {
       "name": "CtsPhotoPickerTest"
     }
   ]
diff --git a/tests/PhotoPicker/res/raw/lg_g4_iso_800_svg.svg b/tests/PhotoPicker/res/raw/lg_g4_iso_800_svg.svg
new file mode 100644
index 0000000..55b3d2e
--- /dev/null
+++ b/tests/PhotoPicker/res/raw/lg_g4_iso_800_svg.svg
Binary files differ
diff --git a/tests/PhotoPicker/res/raw/lg_g4_iso_800_unknown_mime_type.jpg b/tests/PhotoPicker/res/raw/lg_g4_iso_800_unknown_mime_type.jpg
new file mode 100644
index 0000000..c988c57
--- /dev/null
+++ b/tests/PhotoPicker/res/raw/lg_g4_iso_800_unknown_mime_type.jpg
Binary files differ
diff --git a/tests/PhotoPicker/res/raw/test_video_dng.mp4 b/tests/PhotoPicker/res/raw/test_video_mj2.mp4
similarity index 99%
copy from tests/PhotoPicker/res/raw/test_video_dng.mp4
copy to tests/PhotoPicker/res/raw/test_video_mj2.mp4
index 9b38f0e..2b7decd 100644
--- a/tests/PhotoPicker/res/raw/test_video_dng.mp4
+++ b/tests/PhotoPicker/res/raw/test_video_mj2.mp4
Binary files differ
diff --git a/tests/PhotoPicker/res/raw/test_video_dng.mp4 b/tests/PhotoPicker/res/raw/test_video_mpeg.mpeg
similarity index 99%
rename from tests/PhotoPicker/res/raw/test_video_dng.mp4
rename to tests/PhotoPicker/res/raw/test_video_mpeg.mpeg
index 9b38f0e..27b288e 100644
--- a/tests/PhotoPicker/res/raw/test_video_dng.mp4
+++ b/tests/PhotoPicker/res/raw/test_video_mpeg.mpeg
Binary files differ
diff --git a/tests/PhotoPicker/res/raw/test_video_dng.mp4 b/tests/PhotoPicker/res/raw/test_video_unknown_mime_type.mp4
similarity index 99%
copy from tests/PhotoPicker/res/raw/test_video_dng.mp4
copy to tests/PhotoPicker/res/raw/test_video_unknown_mime_type.mp4
index 9b38f0e..5a2e7b5 100644
--- a/tests/PhotoPicker/res/raw/test_video_dng.mp4
+++ b/tests/PhotoPicker/res/raw/test_video_unknown_mime_type.mp4
Binary files differ
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java b/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java
new file mode 100644
index 0000000..5744234
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/ActionGetContentOnlyTest.java
@@ -0,0 +1,292 @@
+/*
+ * 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.photopicker.cts;
+
+import static android.photopicker.cts.util.GetContentActivityAliasUtils.clearPackageData;
+import static android.photopicker.cts.util.GetContentActivityAliasUtils.getDocumentsUiPackageName;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUriAndPath;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findAndClickBrowse;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertReadOnlyAccess;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.ClipData;
+import android.content.Intent;
+import android.net.Uri;
+import android.photopicker.cts.util.GetContentActivityAliasUtils;
+import android.photopicker.cts.util.UiAssertionUtils;
+import android.util.Pair;
+
+import androidx.test.uiautomator.UiObject;
+import androidx.test.uiautomator.UiObjectNotFoundException;
+import androidx.test.uiautomator.UiScrollable;
+import androidx.test.uiautomator.UiSelector;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Photo Picker tests for PhotoPicker launched via {@link Intent#ACTION_GET_CONTENT} intent
+ * exclusively.
+ */
+public class ActionGetContentOnlyTest extends PhotoPickerBaseTest {
+
+    public static final String TAG = "ActionGetContentOnlyTest";
+
+    private static String sDocumentsUiPackageName;
+    private static int sGetContentTakeOverActivityAliasState;
+
+    private List<Uri> mUriList = new ArrayList<>();
+
+    @After
+    public void tearDown() throws Exception {
+        for (Uri uri : mUriList) {
+            deleteMedia(uri, mContext);
+        }
+        mUriList.clear();
+
+        if (mActivity != null) {
+            mActivity.finish();
+        }
+
+        GetContentActivityAliasUtils.restoreState(sGetContentTakeOverActivityAliasState);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        sDocumentsUiPackageName = getDocumentsUiPackageName();
+        sGetContentTakeOverActivityAliasState = GetContentActivityAliasUtils.enableAndGetOldState();
+        clearPackageData(sDocumentsUiPackageName);
+    }
+
+    @Test
+    public void testMimeTypeFilter() throws Exception {
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        intent.setType("audio/*");
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        sDevice.waitForIdle();
+        // Should open documentsUi
+        assertThatShowsDocumentsUiButtons();
+
+        // We don't test the result of the picker here because the intention of the test is only to
+        // test that DocumentsUi is opened.
+    }
+
+    @Test
+    public void testExtraMimeTypeFilter() throws Exception {
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        intent.setType("image/*");
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"video/*", "audio/*"});
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        sDevice.waitForIdle();
+        // Should open documentsUi
+        assertThatShowsDocumentsUiButtons();
+
+        // We don't test the result of the picker here because the intention of the test is only to
+        // test that DocumentsUi is opened.
+    }
+
+    @Test
+    public void testBrowse_singleSelect() throws Exception {
+        final int itemCount = 1;
+        List<Pair<Uri, String>> createdImagesData = createImagesAndGetUriAndPath(itemCount,
+                mContext.getUserId(), /* isFavorite */ false);
+
+        final List<String> fileNameList = new ArrayList<>();
+        for (Pair<Uri, String> createdImageData: createdImagesData) {
+            mUriList.add(createdImageData.first);
+            fileNameList.add(createdImageData.second);
+        }
+
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.setType("image/*");
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+
+        findAndClickBrowse(sDevice);
+
+        findAndClickFilesInDocumentsUi(fileNameList);
+
+        final Uri uri = mActivity.getResult().data.getData();
+
+        assertReadOnlyAccess(uri, mContext.getContentResolver());
+    }
+
+    @Test
+    public void testBrowse_multiSelect() throws Exception {
+        final int itemCount = 3;
+        List<Pair<Uri, String>> createdImagesData = createImagesAndGetUriAndPath(itemCount,
+                mContext.getUserId(), /* isFavorite */ false);
+
+        final List<String> fileNameList = new ArrayList<>();
+        for (Pair<Uri, String> createdImageData: createdImagesData) {
+            mUriList.add(createdImageData.first);
+            fileNameList.add(createdImageData.second);
+        }
+
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+        intent.setType("image/*");
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+
+        findAndClickBrowse(sDevice);
+
+        findAndClickFilesInDocumentsUi(fileNameList);
+
+        final ClipData clipData = mActivity.getResult().data.getClipData();
+        final int count = clipData.getItemCount();
+        assertThat(count).isEqualTo(itemCount);
+        for (int i = 0; i < count; i++) {
+            assertReadOnlyAccess(clipData.getItemAt(i).getUri(), mContext.getContentResolver());
+        }
+    }
+
+    @Test
+    public void testChooserIntent_mediaFilter() throws Exception {
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.setType("image/*");
+        mActivity.startActivityForResult(Intent.createChooser(intent, TAG), REQUEST_CODE);
+
+        // Should open Picker
+        UiAssertionUtils.assertThatShowsPickerUi();
+    }
+
+    @Test
+    public void testChooserIntent_nonMediaFilter() throws Exception {
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.setType("*/*");
+        mActivity.startActivityForResult(Intent.createChooser(intent, TAG), REQUEST_CODE);
+
+        // Should open DocumentsUi
+        assertThatShowsDocumentsUiButtons();
+    }
+
+    @Test
+    public void testPickerSupportedFromDocumentsUi() throws Exception {
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.setType("*/*");
+        mActivity.startActivityForResult(Intent.createChooser(intent, TAG), REQUEST_CODE);
+
+        findAndClickMediaIcon();
+
+        // Should open Picker
+        UiAssertionUtils.assertThatShowsPickerUi();
+    }
+
+    private void findAndClickMediaIcon() throws Exception {
+        final UiSelector appList = new UiSelector().resourceId(sDocumentsUiPackageName
+                + ":id/apps_row");
+
+        // Wait for the first app list item to appear
+        assertWithMessage("Waiting for app list to appear in DocumentsUi").that(
+                new UiObject(appList).waitForExists(SHORT_TIMEOUT)).isTrue();
+
+        String photoPickerAppName = "Media";
+        UiObject mediaButton = sDevice.findObject(new UiSelector().text(photoPickerAppName));
+
+        assertWithMessage("Timed out waiting for " + photoPickerAppName + " app icon to appear")
+                .that(new UiScrollable(appList).setAsHorizontalList().scrollIntoView(mediaButton))
+                .isTrue();
+        sDevice.waitForIdle();
+
+        clickAndWait(sDevice, mediaButton);
+    }
+
+    private void assertThatShowsDocumentsUiButtons() {
+        // Assert that "Recent files" header for DocumentsUi shows
+        // Add a short timeout wait for DocumentsUi to show
+        assertThat(new UiObject(new UiSelector().resourceId(sDocumentsUiPackageName
+                + ":id/header_title")).waitForExists(SHORT_TIMEOUT)).isTrue();
+    }
+
+    private UiObject findSaveButton() {
+        return new UiObject(new UiSelector().resourceId(
+                        sDocumentsUiPackageName + ":id/container_save")
+                .childSelector(new UiSelector().resourceId("android:id/button1")));
+    }
+
+    private void findAndClickFilesInDocumentsUi(List<String> fileNameList) throws Exception {
+        for (String fileName : fileNameList) {
+            findAndClickFileInDocumentsUi(fileName);
+        }
+        findAndClickSelect();
+    }
+
+    private void findAndClickSelect() throws Exception {
+        final UiObject selectButton = new UiObject(new UiSelector().resourceId(
+                sDocumentsUiPackageName + ":id/action_menu_select"));
+        clickAndWait(sDevice, selectButton);
+    }
+
+    private void findAndClickFileInDocumentsUi(String fileName) throws Exception {
+        final UiSelector docList = new UiSelector().resourceId(sDocumentsUiPackageName
+                + ":id/dir_list");
+
+        // Wait for the first list item to appear
+        assertWithMessage("First list item").that(
+                new UiObject(docList.childSelector(new UiSelector()))
+                        .waitForExists(SHORT_TIMEOUT)).isTrue();
+
+        try {
+            // Enforce to set the list mode
+            // Because UiScrollable can't reach the real bottom (when WEB_LINKABLE_FILE item)
+            // in grid mode when screen landscape mode
+            clickAndWait(sDevice, new UiObject(new UiSelector().resourceId(sDocumentsUiPackageName
+                    + ":id/sub_menu_list")));
+        } catch (UiObjectNotFoundException ignored) {
+            // Do nothing, already be in list mode.
+        }
+
+        // Repeat swipe gesture to find our item
+        // (UiScrollable#scrollIntoView does not seem to work well with SwipeRefreshLayout)
+        UiObject targetObject = new UiObject(docList.childSelector(new UiSelector()
+                .textContains(fileName)));
+        UiObject saveButton = findSaveButton();
+        int stepLimit = 10;
+        while (stepLimit-- > 0) {
+            if (targetObject.exists()) {
+                boolean targetObjectFullyVisible = !saveButton.exists()
+                        || targetObject.getVisibleBounds().bottom
+                        <= saveButton.getVisibleBounds().top;
+                if (targetObjectFullyVisible) {
+                    break;
+                }
+            }
+
+            sDevice.swipe(/* startX= */ sDevice.getDisplayWidth() / 2,
+                    /* startY= */ sDevice.getDisplayHeight() / 2,
+                    /* endX= */ sDevice.getDisplayWidth() / 2,
+                    /* endY= */ 0,
+                    /* steps= */ 40);
+        }
+
+        targetObject.longClick();
+    }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java b/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java
new file mode 100644
index 0000000..76ff217
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/ActionPickImagesOnlyTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.photopicker.cts;
+
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertPersistedGrant;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertPickerUriFormat;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertRedactedReadOnlyAccess;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.Intent;
+import android.net.Uri;
+import android.photopicker.cts.util.UiAssertionUtils;
+import android.provider.MediaStore;
+import android.util.Log;
+
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.UiObject;
+import androidx.test.uiautomator.UiSelector;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Photo Picker tests for {@link MediaStore#ACTION_PICK_IMAGES} intent exclusively
+ */
+@RunWith(AndroidJUnit4.class)
+public class ActionPickImagesOnlyTest extends PhotoPickerBaseTest {
+
+    private static final String TAG = "ActionPickImagesOnlyTest";
+    private List<Uri> mUriList = new ArrayList<>();
+
+    @After
+    public void tearDown() throws Exception {
+        for (Uri uri : mUriList) {
+            deleteMedia(uri, mContext);
+        }
+        mUriList.clear();
+
+        if (mActivity != null) {
+            mActivity.finish();
+        }
+    }
+
+    @Test
+    public void testPhotoPickerIntentDelegation() throws Exception {
+        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+
+        for (String mimeType: new String[] {
+                null,
+                "image/*",
+                "video/*"
+        }) {
+            Log.d(TAG, "Testing Photo Picker intent delegation with MimeType " + mimeType);
+            intent.setType(mimeType);
+
+            mActivity.startActivityForResult(Intent.createChooser(intent, TAG), REQUEST_CODE);
+
+            UiAssertionUtils.assertThatShowsPickerUi();
+            sDevice.pressBack();
+        }
+    }
+
+    @Test
+    public void testMultiSelect_invalidParam() throws Exception {
+        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit() + 1);
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        final GetResultActivity.Result res = mActivity.getResult();
+        assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
+    }
+
+    @Test
+    public void testMultiSelect_invalidNegativeParam() throws Exception {
+        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, -1);
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        final GetResultActivity.Result res = mActivity.getResult();
+        assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
+    }
+
+    @Test
+    public void testMultiSelect_returnsNotMoreThanMax() throws Exception {
+        final int maxCount = 2;
+        final int imageCount = maxCount + 1;
+        mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+
+        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxCount);
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+
+        final List<UiObject> itemList = findItemList(imageCount);
+        final int itemCount = itemList.size();
+        assertThat(itemCount).isEqualTo(imageCount);
+        // Select maxCount + 1 item
+        for (int i = 0; i < itemCount; i++) {
+            clickAndWait(sDevice, itemList.get(i));
+        }
+
+        UiObject snackbarTextView = sDevice.findObject(new UiSelector().text(
+                "Select up to 2 items"));
+        assertWithMessage("Timed out while waiting for snackbar to appear").that(
+                snackbarTextView.waitForExists(SHORT_TIMEOUT)).isTrue();
+
+        assertWithMessage("Timed out waiting for snackbar to disappear").that(
+                snackbarTextView.waitUntilGone(SHORT_TIMEOUT)).isTrue();
+
+        clickAndWait(sDevice, findAddButton());
+
+        final ClipData clipData = mActivity.getResult().data.getClipData();
+        final int count = clipData.getItemCount();
+        assertThat(count).isEqualTo(maxCount);
+    }
+
+    @Test
+    public void testDoesNotRespectExtraAllowMultiple() throws Exception {
+        final int imageCount = 2;
+        mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+
+        final List<UiObject> itemList = findItemList(imageCount);
+        final int itemCount = itemList.size();
+        assertThat(itemCount).isEqualTo(imageCount);
+        // Select 1 item
+        clickAndWait(sDevice, itemList.get(0));
+
+        final Uri uri = mActivity.getResult().data.getData();
+        assertPickerUriFormat(uri, mContext.getUserId());
+        assertPersistedGrant(uri, mContext.getContentResolver());
+        assertRedactedReadOnlyAccess(uri);
+    }
+
+    @Test
+    public void testMimeTypeFilter() throws Exception {
+        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+        intent.setType("audio/*");
+        assertThrows(ActivityNotFoundException.class,
+                () -> mActivity.startActivityForResult(intent, REQUEST_CODE));
+    }
+
+    @Test
+    public void testExtraMimeTypeFilter() throws Exception {
+        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"audio/*"});
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        final GetResultActivity.Result res = mActivity.getResult();
+        assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
+    }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java b/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java
index b6f02a5..4c80b64 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/CloudPhotoPickerTest.java
@@ -16,15 +16,16 @@
 
 package android.photopicker.cts;
 
-import static android.os.SystemProperties.getBoolean;
+import static android.photopicker.cts.PhotoPickerCloudUtils.addImage;
+import static android.photopicker.cts.PhotoPickerCloudUtils.containsExcept;
+import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
+import static android.photopicker.cts.PhotoPickerCloudUtils.extractMediaIds;
 import static android.photopicker.cts.PickerProviderMediaGenerator.MediaGenerator;
 import static android.photopicker.cts.PickerProviderMediaGenerator.setCloudProvider;
 import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider;
-import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertRedactedReadOnlyAccess;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImages;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
 import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
-import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertRedactedReadOnlyAccess;
 import static android.provider.MediaStore.PickerMediaColumns;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -47,12 +48,9 @@
 
 import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
-import androidx.test.uiautomator.UiObject;
 
 import org.junit.After;
-import org.junit.Assume;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -66,6 +64,7 @@
  * Photo Picker Device only tests for common flows.
  */
 @RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class CloudPhotoPickerTest extends PhotoPickerBaseTest {
     private final List<Uri> mUriList = new ArrayList<>();
     private MediaGenerator mCloudPrimaryMediaGenerator;
@@ -94,9 +93,10 @@
         mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
         mCloudSecondaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
 
+        // This is a self-instrumentation test, so both "target" package name and "own" package name
+        // should be the same (android.photopicker.cts).
+        enableCloudMediaAndSetAllowedCloudProviders(sTargetPackageName);
         setCloudProvider(mContext, null);
-
-        Assume.assumeTrue(getBoolean("sys.photopicker.pickerdb.enabled", true));
     }
 
     @After
@@ -114,48 +114,44 @@
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testCloudOnlySync() throws Exception {
         initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1));
 
-        final ClipData clipData = fetchPickerMedia(1);
+        final ClipData clipData = launchPickerAndFetchMedia(1);
         final List<String> mediaIds = extractMediaIds(clipData, 1);
 
         assertThat(mediaIds).containsExactly(CLOUD_ID1);
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testCloudPlusLocalSyncWithoutDedupe() throws Exception {
-        createImages(1, mContext.getUserId(), mUriList);
+        mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId()));
         initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1));
 
-        final ClipData clipData = fetchPickerMedia(2);
+        final ClipData clipData = launchPickerAndFetchMedia(2);
         final List<String> mediaIds = extractMediaIds(clipData, 2);
 
         assertThat(mediaIds).containsExactly(CLOUD_ID1, mUriList.get(0).getLastPathSegment());
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testCloudPlusLocalSyncWithDedupe() throws Exception {
-        createImages(1, mContext.getUserId(), mUriList);
+        mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId()));
         initPrimaryCloudProviderWithImage(Pair.create(mUriList.get(0).getLastPathSegment(),
                         CLOUD_ID1));
 
-        final ClipData clipData = fetchPickerMedia(1);
+        final ClipData clipData = launchPickerAndFetchMedia(1);
         final List<String> mediaIds = extractMediaIds(clipData, 1);
 
         containsExcept(mediaIds, mUriList.get(0).getLastPathSegment(), CLOUD_ID1);
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testDeleteCloudMedia() throws Exception {
         initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1),
                 Pair.create(null, CLOUD_ID2));
 
-        ClipData clipData = fetchPickerMedia(2);
+        ClipData clipData = launchPickerAndFetchMedia(2);
         List<String> mediaIds = extractMediaIds(clipData, 2);
 
         assertThat(mediaIds).containsExactly(CLOUD_ID1, CLOUD_ID2);
@@ -164,19 +160,18 @@
                 /* trackDeleted */ true);
         syncCloudProvider(mContext);
 
-        clipData = fetchPickerMedia(2);
+        clipData = launchPickerAndFetchMedia(2);
         mediaIds = extractMediaIds(clipData, 1);
 
         containsExcept(mediaIds, CLOUD_ID2, CLOUD_ID1);
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testVersionChange() throws Exception {
         initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1),
                 Pair.create(null, CLOUD_ID2));
 
-        ClipData clipData = fetchPickerMedia(2);
+        ClipData clipData = launchPickerAndFetchMedia(2);
         List<String> mediaIds = extractMediaIds(clipData, 2);
 
         assertThat(mediaIds).containsExactly(CLOUD_ID1, CLOUD_ID2);
@@ -185,7 +180,7 @@
                 /* trackDeleted */ false);
         syncCloudProvider(mContext);
 
-        clipData = fetchPickerMedia(2);
+        clipData = launchPickerAndFetchMedia(2);
         mediaIds = extractMediaIds(clipData, 2);
 
         assertThat(mediaIds).containsExactly(CLOUD_ID1, CLOUD_ID2);
@@ -193,14 +188,13 @@
         mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_2);
         syncCloudProvider(mContext);
 
-        clipData = fetchPickerMedia(2);
+        clipData = launchPickerAndFetchMedia(2);
         mediaIds = extractMediaIds(clipData, 1);
 
         containsExcept(mediaIds, CLOUD_ID2, CLOUD_ID1);
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testSupportedProviders() throws Exception {
         assertThat(MediaStore.isSupportedCloudMediaProviderAuthority(mContext.getContentResolver(),
                         CloudProviderPrimary.AUTHORITY)).isTrue();
@@ -214,7 +208,6 @@
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testProviderSwitchSuccess() throws Exception {
         setCloudProvider(mContext, CloudProviderPrimary.AUTHORITY);
         assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
@@ -225,7 +218,7 @@
 
         syncCloudProvider(mContext);
 
-        ClipData clipData = fetchPickerMedia(2);
+        ClipData clipData = launchPickerAndFetchMedia(2);
         List<String> mediaIds = extractMediaIds(clipData, 1);
 
         containsExcept(mediaIds, CLOUD_ID1, CLOUD_ID2);
@@ -234,14 +227,13 @@
         assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
                         CloudProviderPrimary.AUTHORITY)).isFalse();
 
-        clipData = fetchPickerMedia(2);
+        clipData = launchPickerAndFetchMedia(2);
         mediaIds = extractMediaIds(clipData, 1);
 
         containsExcept(mediaIds, CLOUD_ID2, CLOUD_ID1);
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testProviderSwitchFailure() throws Exception {
         setCloudProvider(mContext, CloudProviderNoIntentFilter.AUTHORITY);
         assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
@@ -253,11 +245,10 @@
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testUriAccessWithValidProjection() throws Exception {
         initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1));
 
-        final ClipData clipData = fetchPickerMedia(1);
+        final ClipData clipData = launchPickerAndFetchMedia(1);
         final List<String> mediaIds = extractMediaIds(clipData, 1);
 
         assertThat(mediaIds).containsExactly(CLOUD_ID1);
@@ -289,29 +280,32 @@
     }
 
     @Test
-    @Ignore("Ignored in Android T becauase this changed between T and U")
     public void testUriAccessWithInvalidProjection() throws Exception {
         initPrimaryCloudProviderWithImage(Pair.create(null, CLOUD_ID1));
 
-        final ClipData clipData = fetchPickerMedia(1);
+        final ClipData clipData = launchPickerAndFetchMedia(1);
         final List<String> mediaIds = extractMediaIds(clipData, 1);
 
         assertThat(mediaIds).containsExactly(CLOUD_ID1);
 
         final ContentResolver resolver = mContext.getContentResolver();
+        try (Cursor c = resolver.query(
+                clipData.getItemAt(0).getUri(),
+                new String[] {MediaStore.MediaColumns.RELATIVE_PATH}, null, null)) {
+            assertThat(c).isNotNull();
+            assertThat(c.moveToFirst()).isTrue();
 
-        assertThrows(IllegalArgumentException.class, () -> resolver.query(
-            clipData.getItemAt(0).getUri(),
-            new String[] {MediaStore.MediaColumns.RELATIVE_PATH}, null, null));
+            assertThat(c.getString(c.getColumnIndex(MediaStore.MediaColumns.RELATIVE_PATH)))
+                    .isEqualTo(null);
+        }
     }
 
     @Test
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testCloudEventNotification() throws Exception {
         // Create a placeholder local image to ensure that the picker UI is never empty.
         // The PhotoPickerUiUtils#findItemList needs to select an item and it times out if the
         // Picker UI is empty.
-        createImages(1, mContext.getUserId(), mUriList);
+        mUriList.addAll(createImagesAndGetUris(1, mContext.getUserId()));
 
         // Cloud provider isn't set
         assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
@@ -326,7 +320,7 @@
         // Sleep because the notification API throttles requests with a 1s delay
         Thread.sleep(1500);
 
-        ClipData clipData = fetchPickerMedia(1);
+        ClipData clipData = launchPickerAndFetchMedia(1);
         List<String> mediaIds = extractMediaIds(clipData, 1);
 
         assertThat(mediaIds).containsNoneIn(Collections.singletonList(CLOUD_ID1));
@@ -346,7 +340,7 @@
         // Sleep because the notification API throttles requests with a 1s delay
         Thread.sleep(1500);
 
-        clipData = fetchPickerMedia(1);
+        clipData = launchPickerAndFetchMedia(1);
         mediaIds = extractMediaIds(clipData, 1);
 
         assertThat(mediaIds).containsExactly(CLOUD_ID1);
@@ -354,7 +348,6 @@
 
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
-    @Ignore("Ignored in Android T because this changed between T and U")
     public void testStorageManagerKnowsCloudProvider() {
         final StorageManager storageManager = mContext.getSystemService(StorageManager.class);
 
@@ -370,60 +363,17 @@
         assertThat(storageManager.getCloudMediaProvider()).isNull();
     }
 
-    private List<String> extractMediaIds(ClipData clipData, int minCount) {
-        final int count = clipData.getItemCount();
-        assertThat(count).isAtLeast(minCount);
-
-        final List<String> mediaIds = new ArrayList<>();
-        for (int i = 0; i < count; i++) {
-            mediaIds.add(clipData.getItemAt(i).getUri().getLastPathSegment());
-        }
-
-        return mediaIds;
-    }
-
-    private ClipData fetchPickerMedia(int maxCount) throws Exception {
+    private ClipData launchPickerAndFetchMedia(int maxCount) throws Exception {
         final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
         intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
         mActivity.startActivityForResult(intent, REQUEST_CODE);
 
-        final List<UiObject> itemList = findItemList(maxCount);
-        for (int i = 0; i < itemList.size(); i++) {
-            final UiObject item = itemList.get(i);
-            item.click();
-            mDevice.waitForIdle();
-        }
-
-        final UiObject addButton = findAddButton();
-        addButton.click();
-        mDevice.waitForIdle();
-
-        return mActivity.getResult().data.getClipData();
+        return PhotoPickerCloudUtils.fetchPickerMedia(mActivity, sDevice, maxCount);
     }
 
     private void initPrimaryCloudProviderWithImage(Pair<String, String>... mediaPairs)
             throws Exception {
-        setCloudProvider(mContext, CloudProviderPrimary.AUTHORITY);
-        assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
-                        CloudProviderPrimary.AUTHORITY)).isTrue();
-
-        for (Pair<String, String> pair: mediaPairs) {
-            addImage(mCloudPrimaryMediaGenerator, pair.first, pair.second);
-        }
-
-        syncCloudProvider(mContext);
-    }
-
-    private void addImage(MediaGenerator generator, String localId, String cloudId)
-            throws Exception {
-        generator.addMedia(localId, cloudId, /* albumId */ null, "image/jpeg",
-                /* mimeTypeExtension */ 0, IMAGE_SIZE_BYTES, /* isFavorite */ false,
-                R.raw.lg_g4_iso_800_jpg);
-    }
-
-    private static void containsExcept(List<String> mediaIds, String contained,
-            String notContained) {
-        assertThat(mediaIds).contains(contained);
-        assertThat(mediaIds).containsNoneIn(Collections.singletonList(notContained));
+        PhotoPickerCloudUtils.initCloudProviderWithImage(mContext, mCloudPrimaryMediaGenerator,
+                CloudProviderPrimary.AUTHORITY, mediaPairs);
     }
 }
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBannersTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBannersTest.java
new file mode 100644
index 0000000..da578ab
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBannersTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.photopicker.cts;
+
+import static android.photopicker.cts.PhotoPickerCloudUtils.disableCloudMediaAndClearAllowedCloudProviders;
+import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
+import static android.photopicker.cts.PhotoPickerCloudUtils.getAllowedProvidersDeviceConfig;
+import static android.photopicker.cts.PhotoPickerCloudUtils.isCloudMediaEnabled;
+import static android.photopicker.cts.PickerProviderMediaGenerator.setCloudProvider;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImage;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.TIMEOUT;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findBannerActionButton;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findBannerDismissButton;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findBannerPrimaryText;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findSettingsOverflowMenuItem;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.getBannerPrimaryText;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.isPhotoPickerVisible;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsActivityIsVisible;
+import static android.provider.MediaStore.ACTION_PICK_IMAGES;
+import static android.provider.MediaStore.EXTRA_PICK_IMAGES_MAX;
+import static android.provider.MediaStore.getPickImagesMaxLimit;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.photopicker.cts.cloudproviders.CloudProviderPrimary;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.uiautomator.UiObject;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Photo Picker Banner Tests for common flows.
+ */
+// TODO(b/195009187): Enabling the banners requires setting allowed_cloud_providers device config.
+//  We currently can't do this in R.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class PhotoPickerBannersTest extends PhotoPickerBaseTest {
+
+    private static boolean sCloudMediaPreviouslyEnabled;
+    private static String sPreviouslyAllowedCloudProviders;
+    private Uri mLocalMediaFileUri;
+
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        // Store the current CMP configs, so that we can reset them at the end of the test.
+        sCloudMediaPreviouslyEnabled = isCloudMediaEnabled();
+        if (sCloudMediaPreviouslyEnabled) {
+            sPreviouslyAllowedCloudProviders = getAllowedProvidersDeviceConfig();
+        }
+
+        // Override the allowed cloud providers config to enable the banners.
+        enableCloudMediaAndSetAllowedCloudProviders(sTargetPackageName);
+    }
+
+    @AfterClass
+    public static void tearDownClass() {
+        // Reset CloudMedia configs.
+        if (sCloudMediaPreviouslyEnabled) {
+            enableCloudMediaAndSetAllowedCloudProviders(sPreviouslyAllowedCloudProviders);
+        } else {
+            disableCloudMediaAndClearAllowedCloudProviders();
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        setCloudProvider(mContext, /* authority */ null);
+
+        // Create a local media file because if there's no media items for the picker grids,
+        // the recycler view gets hidden along with the banners.
+        mLocalMediaFileUri = createImage(mContext.getUserId(), /* isFavorite */ false).first;
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (!isHardwareSupported()) {
+            // No-op, skip tear down if hardware is not supported.
+            return;
+        }
+
+        if (mActivity != null) {
+            mActivity.finish();
+        }
+
+        deleteMedia(mLocalMediaFileUri, mContext);
+
+        setCloudProvider(mContext, /* authority */ null);
+    }
+
+    @Test
+    public void testChooseAppBannerOnDismiss() throws Exception {
+        // 1. Setting up the 'Choose App' banner.
+        setCloudMediaInfoForChooseAppBanner();
+
+        // 2. Assert that the 'Choose App' banner is visible.
+        assertThat(getBannerPrimaryText()).isEqualTo("Choose cloud media app");
+
+        // 3. Click the banner 'Dismiss' button.
+        final UiObject dismissButton = findBannerDismissButton();
+        dismissButton.click();
+
+        // 4. Assert that the Banner disappeared while the Picker is still visible.
+        assertWithMessage("Timed out waiting for the banner to disappear")
+                .that(dismissButton.waitUntilGone(TIMEOUT))
+                .isTrue();
+        assertThatPhotoPickerActivityIsVisible();
+    }
+
+    @Test
+    @Ignore("Tracking this in b/274840171")
+    public void testChooseAppBannerOnActionButtonClick() throws Exception {
+        // 1. Setting up the 'Choose App' banner.
+        setCloudMediaInfoForChooseAppBanner();
+
+        // 2. Assert that the 'Choose App' banner is visible.
+        assertThat(getBannerPrimaryText()).isEqualTo("Choose cloud media app");
+
+        // 3. Click the banner 'Action' button.
+        findBannerActionButton().click();
+
+        // 4. Assert that Settings page is visible.
+        verifySettingsActivityIsVisible();
+        sDevice.pressBack();
+    }
+
+    private void setCloudMediaInfoForChooseAppBanner() throws Exception {
+        // 1. Set a non-null cloud provider and launch the photo picker.
+        setCloudProvider(mContext, CloudProviderPrimary.AUTHORITY);
+        launchPickerActivity();
+
+        // 2. Wait for the banner controller construction.
+        findBannerPrimaryText().waitForExists(TIMEOUT);
+
+        // 3. Set the cloud provider to None and reset the banner data in the same picker session.
+
+        // 3a. Launch the settings activity.
+        final UiObject settingsMenuItem = findSettingsOverflowMenuItem(sDevice);
+        clickAndWait(sDevice, settingsMenuItem);
+        verifySettingsActivityIsVisible();
+
+        // 3b. Set the cloud provider to None.
+        setCloudProvider(mContext, /* authority */ null);
+
+        // 3c. Go back to the picker.
+        sDevice.pressBack();
+        assertThatPhotoPickerActivityIsVisible();
+    }
+
+    private void launchPickerActivity() {
+        final Intent intent = new Intent(ACTION_PICK_IMAGES);
+        intent.putExtra(EXTRA_PICK_IMAGES_MAX, getPickImagesMaxLimit());
+        mActivity.startActivity(intent);
+        assertThatPhotoPickerActivityIsVisible();
+    }
+
+    private void assertThatPhotoPickerActivityIsVisible() {
+        assertWithMessage("Timed out waiting for the photo picker activity to appear")
+                .that(isPhotoPickerVisible())
+                .isTrue();
+    }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBaseTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBaseTest.java
index 197fbd5..067cd78 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBaseTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerBaseTest.java
@@ -16,7 +16,6 @@
 
 package android.photopicker.cts;
 
-import android.Manifest;
 import android.app.Instrumentation;
 import android.content.Context;
 import android.content.Intent;
@@ -25,6 +24,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.UiDevice;
 
+
 import org.junit.Assume;
 import org.junit.Before;
 
@@ -34,42 +34,42 @@
  */
 public class PhotoPickerBaseTest {
     public static int REQUEST_CODE = 42;
+    private static final Instrumentation sInstrumentation =
+            InstrumentationRegistry.getInstrumentation();
+    protected static final String sTargetPackageName =
+            sInstrumentation.getTargetContext().getPackageName();
+    protected static final UiDevice sDevice = UiDevice.getInstance(sInstrumentation);
 
     protected GetResultActivity mActivity;
     protected Context mContext;
-    protected UiDevice mDevice;
 
     @Before
     public void setUp() throws Exception {
         Assume.assumeTrue(isHardwareSupported());
 
-        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
-        mDevice = UiDevice.getInstance(inst);
-
         final String setSyncDelayCommand =
                 "device_config put storage pickerdb.default_sync_delay_ms 0";
-        mDevice.executeShellCommand(setSyncDelayCommand);
+        sDevice.executeShellCommand(setSyncDelayCommand);
 
-        mContext = inst.getContext();
+        mContext = sInstrumentation.getContext();
         final Intent intent = new Intent(mContext, GetResultActivity.class);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
 
         // Wake up the device and dismiss the keyguard before the test starts
-        mDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
-        mDevice.executeShellCommand("wm dismiss-keyguard");
+        sDevice.executeShellCommand("input keyevent KEYCODE_WAKEUP");
+        sDevice.executeShellCommand("wm dismiss-keyguard");
 
-        mActivity = (GetResultActivity) inst.startActivitySync(intent);
+        mActivity = (GetResultActivity) sInstrumentation.startActivitySync(intent);
         // Wait for the UI Thread to become idle.
-        inst.waitForIdleSync();
+        sInstrumentation.waitForIdleSync();
         mActivity.clearResult();
-        mDevice.waitForIdle();
+        sDevice.waitForIdle();
     }
 
-    private static boolean isHardwareSupported() {
-        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+    static boolean isHardwareSupported() {
         // These UI tests are not optimised for Watches, TVs, Auto;
         // IoT devices do not have a UI to run these UI tests
-        PackageManager pm = inst.getContext().getPackageManager();
+        PackageManager pm = sInstrumentation.getContext().getPackageManager();
         return !pm.hasSystemFeature(pm.FEATURE_EMBEDDED)
                 && !pm.hasSystemFeature(pm.FEATURE_WATCH)
                 && !pm.hasSystemFeature(pm.FEATURE_LEANBACK)
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCloudUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCloudUtils.java
new file mode 100644
index 0000000..891917d
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCloudUtils.java
@@ -0,0 +1,173 @@
+/*
+ * 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.photopicker.cts;
+
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.photopicker.cts.PickerProviderMediaGenerator.setCloudProvider;
+import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.app.UiAutomation;
+import android.content.ClipData;
+import android.content.Context;
+import android.provider.DeviceConfig;
+import android.provider.MediaStore;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class PhotoPickerCloudUtils {
+    private static final String NAMESPACE_STORAGE_NATIVE_BOOT = "storage_native_boot";
+    private static final String KEY_ALLOWED_CLOUD_PROVIDERS = "allowed_cloud_providers";
+    private static final String KEY_CLOUD_MEDIA_FEATURE_ENABLED = "cloud_media_feature_enabled";
+
+    public static List<String> extractMediaIds(ClipData clipData, int minCount) {
+        final int count = clipData.getItemCount();
+        assertThat(count).isAtLeast(minCount);
+
+        final List<String> mediaIds = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            mediaIds.add(clipData.getItemAt(i).getUri().getLastPathSegment());
+        }
+
+        return mediaIds;
+    }
+
+    public static void selectAndAddPickerMedia(UiDevice uiDevice, int maxCount)
+            throws Exception {
+        final List<UiObject> itemList = findItemList(maxCount);
+        for (int i = 0; i < itemList.size(); i++) {
+            final UiObject item = itemList.get(i);
+            item.click();
+            uiDevice.waitForIdle();
+        }
+
+        final UiObject addButton = findAddButton();
+        addButton.click();
+        uiDevice.waitForIdle();
+    }
+
+    public static ClipData fetchPickerMedia(GetResultActivity activity, UiDevice uiDevice,
+            int maxCount) throws Exception {
+        selectAndAddPickerMedia(uiDevice, maxCount);
+
+        return activity.getResult().data.getClipData();
+    }
+
+    public static void initCloudProviderWithImage(
+            Context context, PickerProviderMediaGenerator.MediaGenerator mediaGenerator,
+            String authority, Pair<String, String>... mediaPairs) throws Exception {
+        setCloudProvider(context, authority);
+        assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(context.getContentResolver(),
+                authority)).isTrue();
+
+        for (Pair<String, String> pair : mediaPairs) {
+            addImage(mediaGenerator, pair.first, pair.second);
+        }
+
+        syncCloudProvider(context);
+    }
+
+    public static void addImage(PickerProviderMediaGenerator.MediaGenerator generator,
+            String localId, String cloudId)
+            throws Exception {
+        final long imageSizeBytes = 107684;
+        generator.addMedia(localId, cloudId, /* albumId */ null, "image/jpeg",
+                /* mimeTypeExtension */ 0, imageSizeBytes, /* isFavorite */ false,
+                R.raw.lg_g4_iso_800_jpg);
+    }
+
+    public static void containsExcept(List<String> mediaIds, String contained,
+            String notContained) {
+        assertThat(mediaIds).contains(contained);
+        assertThat(mediaIds).containsNoneIn(Collections.singletonList(notContained));
+    }
+
+    public static boolean isCloudMediaEnabled() {
+        return Boolean.parseBoolean(readDeviceConfigProp(KEY_CLOUD_MEDIA_FEATURE_ENABLED));
+    }
+
+    @Nullable
+    static String getAllowedProvidersDeviceConfig() {
+        return readDeviceConfigProp(KEY_ALLOWED_CLOUD_PROVIDERS);
+    }
+
+    static void enableCloudMediaAndSetAllowedCloudProviders(@NonNull String allowedPackagesJoined) {
+        writeDeviceConfigProp(KEY_ALLOWED_CLOUD_PROVIDERS, allowedPackagesJoined);
+        assertWithMessage("Failed to update the allowed cloud providers device config")
+                .that(getAllowedProvidersDeviceConfig())
+                .isEqualTo(allowedPackagesJoined);
+
+        writeDeviceConfigProp(KEY_CLOUD_MEDIA_FEATURE_ENABLED, true);
+    }
+
+
+    static void disableCloudMediaAndClearAllowedCloudProviders() {
+        writeDeviceConfigProp(KEY_CLOUD_MEDIA_FEATURE_ENABLED, false);
+
+        deleteDeviceConfigProp(KEY_ALLOWED_CLOUD_PROVIDERS);
+        assertWithMessage("Failed to delete the allowed cloud providers device config")
+                .that(getAllowedProvidersDeviceConfig())
+                .isNull();
+    }
+
+    @NonNull
+    private static UiAutomation getUiAutomation() {
+        return InstrumentationRegistry.getInstrumentation().getUiAutomation();
+    }
+
+    @Nullable
+    private static String readDeviceConfigProp(@NonNull String name) {
+        getUiAutomation().adoptShellPermissionIdentity(READ_DEVICE_CONFIG);
+        try {
+            return DeviceConfig.getProperty(NAMESPACE_STORAGE_NATIVE_BOOT, name);
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    private static void writeDeviceConfigProp(@NonNull String name, boolean value) {
+        writeDeviceConfigProp(name, Boolean.toString(value));
+    }
+
+    private static void writeDeviceConfigProp(@NonNull String name, @NonNull String value) {
+        getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
+        try {
+            DeviceConfig.setProperty(NAMESPACE_STORAGE_NATIVE_BOOT, name, value,
+                    /* makeDefault*/ false);
+        } finally {
+            getUiAutomation().dropShellPermissionIdentity();
+        }
+    }
+
+    private static void deleteDeviceConfigProp(@NonNull String name) {
+        DeviceConfig.deleteProperty(NAMESPACE_STORAGE_NATIVE_BOOT, name);
+    }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCrossProfileTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCrossProfileTest.java
index 44491e6..8e48673 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCrossProfileTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerCrossProfileTest.java
@@ -16,14 +16,14 @@
 
 package android.photopicker.cts;
 
-import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertPickerUriFormat;
-import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertRedactedReadOnlyAccess;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImages;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
 import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findProfileButton;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertPickerUriFormat;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertRedactedReadOnlyAccess;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -33,6 +33,7 @@
 import android.net.Uri;
 import android.provider.MediaStore;
 
+import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.uiautomator.UiObject;
 import androidx.test.uiautomator.UiSelector;
@@ -56,6 +57,7 @@
  * Photo Picker Device only tests for cross profile interaction flows.
  */
 @RunWith(BedsteadJUnit4.class)
+@LargeTest
 public class PhotoPickerCrossProfileTest extends PhotoPickerBaseTest {
     @ClassRule @Rule
     public static final DeviceState sDeviceState = new DeviceState();
@@ -83,7 +85,7 @@
     @Ignore("Ignored in Android T b/288065034")
     public void testWorkApp_canAccessPersonalProfileContents() throws Exception {
         final int imageCount = 2;
-        createImages(imageCount, sDeviceState.primaryUser().id(), mUriList);
+        mUriList.addAll(createImagesAndGetUris(imageCount, sDeviceState.primaryUser().id()));
 
         Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
         intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, imageCount);
@@ -92,7 +94,7 @@
         // Click the profile button to change to personal profile
         final UiObject profileButton = findProfileButton();
         profileButton.click();
-        mDevice.waitForIdle();
+        sDevice.waitForIdle();
 
         final List<UiObject> itemList = findItemList(imageCount);
         final int itemCount = itemList.size();
@@ -100,12 +102,12 @@
         for (int i = 0; i < itemCount; i++) {
             final UiObject item = itemList.get(i);
             item.click();
-            mDevice.waitForIdle();
+            sDevice.waitForIdle();
         }
 
         final UiObject addButton = findAddButton();
         addButton.click();
-        mDevice.waitForIdle();
+        sDevice.waitForIdle();
 
         final ClipData clipData = mActivity.getResult().data.getClipData();
         final int count = clipData.getItemCount();
@@ -146,7 +148,7 @@
         // Click the profile button to change to work profile
         final UiObject profileButton = findProfileButton();
         profileButton.click();
-        mDevice.waitForIdle();
+        sDevice.waitForIdle();
 
         assertBlockedByAdminDialog(isInvokedFromWorkProfile);
     }
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerSettingsTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerSettingsTest.java
new file mode 100644
index 0000000..70e0502
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerSettingsTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.photopicker.cts;
+
+import static android.photopicker.cts.PhotoPickerCloudUtils.disableCloudMediaAndClearAllowedCloudProviders;
+import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
+import static android.photopicker.cts.PhotoPickerCloudUtils.getAllowedProvidersDeviceConfig;
+import static android.photopicker.cts.PhotoPickerCloudUtils.isCloudMediaEnabled;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.isPhotoPickerVisible;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsActionBarIsVisible;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsActivityIsVisible;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsDescriptionIsVisible;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsFragmentContainerExists;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.verifySettingsTitleIsVisible;
+
+import android.content.Intent;
+import android.os.Build;
+import android.photopicker.cts.util.PhotoPickerUiUtils;
+import android.provider.MediaStore;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.uiautomator.UiObject;
+
+import org.junit.AfterClass;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Photo Picker tests for settings page launched from the overflow menu in PhotoPickerActivity or
+ * the Settings app.
+ */
+// TODO(b/195009187): Enabling settings page requires setting allowed_cloud_providers device config.
+//  We currently can't do this in R.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class PhotoPickerSettingsTest extends PhotoPickerBaseTest {
+
+    private static boolean sCloudMediaPreviouslyEnabled;
+    private static String sPreviouslyAllowedCloudProviders;
+
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        // Store the current CMP configs, so that we can reset them at the end of the test.
+        sCloudMediaPreviouslyEnabled = isCloudMediaEnabled();
+        if (sCloudMediaPreviouslyEnabled) {
+            sPreviouslyAllowedCloudProviders = getAllowedProvidersDeviceConfig();
+        }
+
+        // Enable Settings menu item in PhotoPickerActivity's overflow menu.
+        PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders(
+                /* allowedCloudProviders */ "not_empty");
+    }
+
+    @AfterClass
+    public static void tearDownClass() {
+        // Reset CloudMedia configs.
+        if (sCloudMediaPreviouslyEnabled) {
+            enableCloudMediaAndSetAllowedCloudProviders(sPreviouslyAllowedCloudProviders);
+        } else {
+            disableCloudMediaAndClearAllowedCloudProviders();
+        }
+    }
+
+    @Test
+    public void testSettingsLaunchFromOverflowMenu() throws Exception {
+        // Launch PhotoPickerActivity.
+        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        sDevice.waitForIdle();
+        Assume.assumeTrue("Assume photo picker activity is visible", isPhotoPickerVisible());
+
+        // Click on the Settings menu item in the overflow menu.
+        final UiObject settingsMenuItem = PhotoPickerUiUtils.findSettingsOverflowMenuItem(sDevice);
+        PhotoPickerUiUtils.clickAndWait(sDevice, settingsMenuItem);
+
+        // Verify PhotoPickerSettingsActivity is launched and visible.
+        verifySettingsActivityIsVisible();
+        verifySettingsActionBarIsVisible();
+        verifySettingsTitleIsVisible();
+        verifySettingsDescriptionIsVisible();
+        verifySettingsFragmentContainerExists();
+    }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
index 32ac2fd9..41a4337 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PhotoPickerTest.java
@@ -16,50 +16,86 @@
 
 package android.photopicker.cts;
 
-import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertMimeType;
-import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertPersistedGrant;
-import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertPickerUriFormat;
-import static android.photopicker.cts.util.PhotoPickerAssertionsUtils.assertRedactedReadOnlyAccess;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createDNGVideos;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImages;
-import static android.photopicker.cts.util.PhotoPickerFilesUtils.createVideos;
+import static android.photopicker.cts.util.GetContentActivityAliasUtils.clearPackageData;
+import static android.photopicker.cts.util.GetContentActivityAliasUtils.getDocumentsUiPackageName;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImageWithUnknownMimeType;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createImagesAndGetUris;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createMj2VideosAndGetUris;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createMpegVideo;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createSvgImage;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createVideoWithUnknownMimeType;
+import static android.photopicker.cts.util.PhotoPickerFilesUtils.createVideosAndGetUris;
 import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.REGEX_PACKAGE_NAME;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findPreviewAddButton;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findPreviewAddOrSelectButton;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertContainsMimeType;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertExtension;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertMimeType;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertPersistedGrant;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertPickerUriFormat;
+import static android.photopicker.cts.util.ResultsAssertionsUtils.assertRedactedReadOnlyAccess;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-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.photopicker.cts.util.GetContentActivityAliasUtils;
 import android.provider.MediaStore;
 
-import androidx.test.runner.AndroidJUnit4;
 import androidx.test.uiautomator.UiObject;
 import androidx.test.uiautomator.UiObjectNotFoundException;
 import androidx.test.uiautomator.UiSelector;
 
 import org.junit.After;
+import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Photo Picker Device only tests for common flows.
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(Parameterized.class)
 public class PhotoPickerTest extends PhotoPickerBaseTest {
+
+    @Parameter(0)
+    public String mAction;
+
+    @Parameters(name = "intent={0}")
+    public static Iterable<? extends Object> data() {
+        return getTestParameters();
+    }
+
     private List<Uri> mUriList = new ArrayList<>();
 
+    private static int sGetContentTakeOverActivityAliasState;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        sGetContentTakeOverActivityAliasState = GetContentActivityAliasUtils.enableAndGetOldState();
+        clearPackageData(getDocumentsUiPackageName());
+    }
+
     @After
     public void tearDown() throws Exception {
         for (Uri uri : mUriList) {
@@ -70,19 +106,21 @@
         if (mActivity != null) {
             mActivity.finish();
         }
+
+        GetContentActivityAliasUtils.restoreState(sGetContentTakeOverActivityAliasState);
     }
 
     @Test
     @Ignore("Ignored in Android T b/288065034")
     public void testSingleSelect() throws Exception {
         final int itemCount = 1;
-        createImages(itemCount, mContext.getUserId(), mUriList);
+        mUriList.addAll(createImagesAndGetUris(itemCount, mContext.getUserId()));
 
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        final Intent intent = new Intent(mAction);
+        launchPhotoPickerForIntent(intent);
 
         final UiObject item = findItemList(itemCount).get(0);
-        clickAndWait(item);
+        clickAndWait(sDevice, item);
 
         final Uri uri = mActivity.getResult().data.getData();
         assertPickerUriFormat(uri, mContext.getUserId());
@@ -94,19 +132,20 @@
     @Ignore("Ignored in Android T b/288065034")
     public void testSingleSelectForFavoritesAlbum() throws Exception {
         final int itemCount = 1;
-        createImages(itemCount, mContext.getUserId(), mUriList, true);
+        mUriList.addAll(createImagesAndGetUris(itemCount, mContext.getUserId(),
+                /* isFavorite */ true));
 
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        final Intent intent = new Intent(mAction);
+        launchPhotoPickerForIntent(intent);
 
-        UiObject albumsTab = mDevice.findObject(new UiSelector().text(
+        UiObject albumsTab = sDevice.findObject(new UiSelector().text(
                 "Albums"));
-        clickAndWait(albumsTab);
+        clickAndWait(sDevice, albumsTab);
         final UiObject album = findItemList(1).get(0);
-        clickAndWait(album);
+        clickAndWait(sDevice, album);
 
         final UiObject item = findItemList(itemCount).get(0);
-        clickAndWait(item);
+        clickAndWait(sDevice, item);
 
         final Uri uri = mActivity.getResult().data.getData();
         assertPickerUriFormat(uri, mContext.getUserId());
@@ -117,18 +156,18 @@
     @Ignore("Ignored in Android T b/288065034")
     public void testLaunchPreviewMultipleForVideoAlbum() throws Exception {
         final int videoCount = 2;
-        createVideos(videoCount, mContext.getUserId(), mUriList);
+        mUriList.addAll(createVideosAndGetUris(videoCount, mContext.getUserId()));
 
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
+        Intent intent = new Intent(mAction);
         intent.setType("video/*");
-        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        addMultipleSelectionFlag(intent);
+        launchPhotoPickerForIntent(intent);
 
-        UiObject albumsTab = mDevice.findObject(new UiSelector().text(
+        UiObject albumsTab = sDevice.findObject(new UiSelector().text(
                 "Albums"));
-        clickAndWait(albumsTab);
+        clickAndWait(sDevice, albumsTab);
         final UiObject album = findItemList(1).get(0);
-        clickAndWait(album);
+        clickAndWait(sDevice, album);
 
         final List<UiObject> itemList = findItemList(videoCount);
         final int itemCount = itemList.size();
@@ -136,10 +175,10 @@
         assertThat(itemCount).isEqualTo(videoCount);
 
         for (int i = 0; i < itemCount; i++) {
-            clickAndWait(itemList.get(i));
+            clickAndWait(sDevice, itemList.get(i));
         }
 
-        clickAndWait(findViewSelectedButton());
+        clickAndWait(sDevice, findViewSelectedButton());
 
         // Wait for playback to start. This is needed in some devices where playback
         // buffering -> ready state takes around 10s.
@@ -151,18 +190,18 @@
     @Ignore("Ignored in Android T b/288065034")
     public void testSingleSelectWithPreview() throws Exception {
         final int itemCount = 1;
-        createImages(itemCount, mContext.getUserId(), mUriList);
+        mUriList.addAll(createImagesAndGetUris(itemCount, mContext.getUserId()));
 
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        final Intent intent = new Intent(mAction);
+        launchPhotoPickerForIntent(intent);
 
         final UiObject item = findItemList(itemCount).get(0);
         item.longClick();
-        mDevice.waitForIdle();
+        sDevice.waitForIdle();
 
         final UiObject addButton = findPreviewAddOrSelectButton();
         assertThat(addButton.waitForExists(1000)).isTrue();
-        clickAndWait(addButton);
+        clickAndWait(sDevice, addButton);
 
         final Uri uri = mActivity.getResult().data.getData();
         assertPickerUriFormat(uri, mContext.getUserId());
@@ -170,94 +209,22 @@
     }
 
     @Test
-    public void testMultiSelect_invalidParam() throws Exception {
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit() + 1);
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
-        final GetResultActivity.Result res = mActivity.getResult();
-        assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
-    }
-
-    @Test
-    public void testMultiSelect_invalidNegativeParam() throws Exception {
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, -1);
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
-        final GetResultActivity.Result res = mActivity.getResult();
-        assertThat(res.resultCode).isEqualTo(Activity.RESULT_CANCELED);
-    }
-
-    @Test
-    @Ignore("Ignored in Android T b/288065034")
-    public void testMultiSelect_returnsNotMoreThanMax() throws Exception {
-        final int maxCount = 2;
-        final int imageCount = maxCount + 1;
-        createImages(imageCount, mContext.getUserId(), mUriList);
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxCount);
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
-
-        final List<UiObject> itemList = findItemList(imageCount);
-        final int itemCount = itemList.size();
-        assertThat(itemCount).isEqualTo(imageCount);
-        // Select maxCount + 1 item
-        for (int i = 0; i < itemCount; i++) {
-            clickAndWait(itemList.get(i));
-        }
-
-        UiObject snackbarTextView = mDevice.findObject(new UiSelector().text(
-                "Select up to 2 items"));
-        assertWithMessage("Timed out while waiting for snackbar to appear").that(
-                snackbarTextView.waitForExists(SHORT_TIMEOUT)).isTrue();
-
-        assertWithMessage("Timed out waiting for snackbar to disappear").that(
-                snackbarTextView.waitUntilGone(SHORT_TIMEOUT)).isTrue();
-
-        clickAndWait(findAddButton());
-
-        final ClipData clipData = mActivity.getResult().data.getClipData();
-        final int count = clipData.getItemCount();
-        assertThat(count).isEqualTo(maxCount);
-    }
-
-    @Test
-    @Ignore("Ignored in Android T b/288065034")
-    public void testDoesNotRespectExtraAllowMultiple() throws Exception {
-        final int imageCount = 2;
-        createImages(imageCount, mContext.getUserId(), mUriList);
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
-
-        final List<UiObject> itemList = findItemList(imageCount);
-        final int itemCount = itemList.size();
-        assertThat(itemCount).isEqualTo(imageCount);
-        // Select 1 item
-        clickAndWait(itemList.get(0));
-
-        final Uri uri = mActivity.getResult().data.getData();
-        assertPickerUriFormat(uri, mContext.getUserId());
-        assertPersistedGrant(uri, mContext.getContentResolver());
-        assertRedactedReadOnlyAccess(uri);
-    }
-
-    @Test
     @Ignore("Ignored in Android T b/288065034")
     public void testMultiSelect() throws Exception {
         final int imageCount = 4;
-        createImages(imageCount, mContext.getUserId(), mUriList);
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+        Intent intent = new Intent(mAction);
+        addMultipleSelectionFlag(intent);
+        launchPhotoPickerForIntent(intent);
 
         final List<UiObject> itemList = findItemList(imageCount);
         final int itemCount = itemList.size();
         assertThat(itemCount).isEqualTo(imageCount);
         for (int i = 0; i < itemCount; i++) {
-            clickAndWait(itemList.get(i));
+            clickAndWait(sDevice, itemList.get(i));
         }
 
-        clickAndWait(findAddButton());
+        clickAndWait(sDevice, findAddButton());
 
         final ClipData clipData = mActivity.getResult().data.getClipData();
         final int count = clipData.getItemCount();
@@ -274,37 +241,38 @@
     @Ignore("Ignored in Android T b/288065034")
     public void testMultiSelect_longPress() throws Exception {
         final int videoCount = 3;
-        createDNGVideos(videoCount, mContext.getUserId(), mUriList);
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
+        mUriList.addAll(createMj2VideosAndGetUris(videoCount, mContext.getUserId()));
+
+        Intent intent = new Intent(mAction);
         intent.setType("video/*");
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        addMultipleSelectionFlag(intent);
+        launchPhotoPickerForIntent(intent);
 
         final List<UiObject> itemList = findItemList(videoCount);
         final int itemCount = itemList.size();
         assertThat(itemCount).isEqualTo(videoCount);
 
         // Select one item from Photo grid
-        clickAndWait(itemList.get(0));
+        clickAndWait(sDevice, itemList.get(0));
 
         // Preview the item
         UiObject item = itemList.get(1);
         item.longClick();
-        mDevice.waitForIdle();
+        sDevice.waitForIdle();
 
         final UiObject addOrSelectButton = findPreviewAddOrSelectButton();
         assertWithMessage("Timed out waiting for AddOrSelectButton to appear")
                 .that(addOrSelectButton.waitForExists(1000)).isTrue();
 
         // Select the item from Preview
-        clickAndWait(addOrSelectButton);
+        clickAndWait(sDevice, addOrSelectButton);
 
-        mDevice.pressBack();
+        sDevice.pressBack();
 
         // Select one more item from Photo grid
-        clickAndWait(itemList.get(2));
+        clickAndWait(sDevice, itemList.get(2));
 
-        clickAndWait(findAddButton());
+        clickAndWait(sDevice, findAddButton());
 
         // Verify that all 3 items are returned
         final ClipData clipData = mActivity.getResult().data.getClipData();
@@ -322,19 +290,20 @@
     @Ignore("Ignored in Android T b/288065034")
     public void testMultiSelect_preview() throws Exception {
         final int imageCount = 4;
-        createImages(imageCount, mContext.getUserId(), mUriList);
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+
+        Intent intent = new Intent(mAction);
+        addMultipleSelectionFlag(intent);
+        launchPhotoPickerForIntent(intent);
 
         final List<UiObject> itemList = findItemList(imageCount);
         final int itemCount = itemList.size();
         assertThat(itemCount).isEqualTo(imageCount);
         for (int i = 0; i < itemCount; i++) {
-            clickAndWait(itemList.get(i));
+            clickAndWait(sDevice, itemList.get(i));
         }
 
-        clickAndWait(findViewSelectedButton());
+        clickAndWait(sDevice, findViewSelectedButton());
 
         // Swipe left three times
         swipeLeftAndWait();
@@ -342,10 +311,10 @@
         swipeLeftAndWait();
 
         // Deselect one item
-        clickAndWait(findPreviewSelectedCheckButton());
+        clickAndWait(sDevice, findPreviewSelectedCheckButton());
 
         // Return selected items
-        clickAndWait(findPreviewAddButton());
+        clickAndWait(sDevice, findPreviewAddButton());
 
         final ClipData clipData = mActivity.getResult().data.getClipData();
         final int count = clipData.getItemCount();
@@ -393,20 +362,31 @@
 
         // Test 2: Click Mute Button
         // Click to unmute the audio
-        clickAndWait(muteButton);
+        clickAndWait(sDevice, 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);
+        clickAndWait(sDevice, muteButton);
+
+        waitForBinderCallsToComplete();
+
         assertMuteButtonState(muteButton, /* isMuted */ true);
         // Click on the muteButton and check that mute button status is now unmute
-        clickAndWait(muteButton);
+        clickAndWait(sDevice, muteButton);
+
+        waitForBinderCallsToComplete();
+
         assertMuteButtonState(muteButton, /* isMuted */ false);
 
         // Test 3: Next preview resumes mute state
         // Go back and launch preview again
-        mDevice.pressBack();
-        clickAndWait(findViewSelectedButton());
+        sDevice.pressBack();
+        clickAndWait(sDevice, findViewSelectedButton());
+
+        waitForBinderCallsToComplete();
 
         // check that player controls are visible
         assertPlayerControlsVisible(playPauseButton, muteButton);
@@ -432,16 +412,25 @@
         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);
 
         // 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);
+        clickAndWait(sDevice, 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
@@ -453,6 +442,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(sDevice, muteButton);
+
+        // Remote video preview involves binder calls
+        // Wait for Binder calls to complete and device to be idle
+        MediaStore.waitForIdle(mContext.getContentResolver());
+        sDevice.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);
@@ -464,7 +525,7 @@
 
         final UiObject playerView = findPlayerView();
         // Click on StyledPlayerView to make the video controls visible
-        clickAndWait(playerView);
+        clickAndWait(sDevice, playerView);
         assertPlayerControlsVisible(playPauseButton, muteButton);
 
         // Wait for 1s and check that controls are still visible
@@ -473,7 +534,7 @@
         // Click on StyledPlayerView and check that controls are no longer visible. Don't click in
         // the center, clicking in the center may pause the video.
         playerView.clickBottomRight();
-        mDevice.waitForIdle();
+        sDevice.waitForIdle();
         assertPlayerControlsHidden(playPauseButton, muteButton);
 
         // Swipe left and check that controls are not visible
@@ -481,7 +542,7 @@
         assertPlayerControlsHidden(playPauseButton, muteButton);
 
         // Click on the StyledPlayerView and check that controls appear
-        clickAndWait(playerView);
+        clickAndWait(sDevice, playerView);
         assertPlayerControlsVisible(playPauseButton, muteButton);
 
         // Swipe left to check that controls are now visible on swipe
@@ -499,26 +560,26 @@
     @Ignore("Ignored in Android T b/288065034")
     public void testMimeTypeFilter() throws Exception {
         final int videoCount = 2;
-        createDNGVideos(videoCount, mContext.getUserId(), mUriList);
+        mUriList.addAll(createMj2VideosAndGetUris(videoCount, mContext.getUserId()));
         final int imageCount = 1;
-        createImages(imageCount, mContext.getUserId(), mUriList);
+        mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
 
-        final String mimeType = "video/dng";
+        final String mimeType = "video/mj2";
 
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
+        Intent intent = new Intent(mAction);
+        addMultipleSelectionFlag(intent);
         intent.setType(mimeType);
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        launchPhotoPickerForIntent(intent);
 
         // find all items
         final List<UiObject> itemList = findItemList(-1);
         final int itemCount = itemList.size();
         assertThat(itemCount).isAtLeast(videoCount);
         for (int i = 0; i < itemCount; i++) {
-            clickAndWait(itemList.get(i));
+            clickAndWait(sDevice, itemList.get(i));
         }
 
-        clickAndWait(findAddButton());
+        clickAndWait(sDevice, findAddButton());
 
         final ClipData clipData = mActivity.getResult().data.getClipData();
         final int count = clipData.getItemCount();
@@ -532,6 +593,136 @@
         }
     }
 
+    @Test
+    public void testExtraMimeTypeFilter() throws Exception {
+        final int mj2VideoCount = 2;
+        // Creates 2 videos with mime type: "video/mj2"
+        mUriList.addAll(createMj2VideosAndGetUris(mj2VideoCount, mContext.getUserId()));
+
+        final int mp4VideoCount = 3;
+        // Creates 3 videos with mime type: "video/mp4"
+        mUriList.addAll(createVideosAndGetUris(mp4VideoCount, mContext.getUserId()));
+
+        final int imageCount = 4;
+        // Creates 4 images with mime type: "image/dng"
+        mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+
+        Intent intent = new Intent(mAction);
+        addMultipleSelectionFlag(intent);
+
+        if (Intent.ACTION_GET_CONTENT.equals(intent.getAction())) {
+            intent.setType("*/*");
+        }
+        final String[] mimeTypes = new String[]{"video/mj2", "image/dng"};
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
+        launchPhotoPickerForIntent(intent);
+
+        final int totalCount = mj2VideoCount + imageCount;
+        final List<UiObject> itemList = findItemList(totalCount);
+        final int itemCount = itemList.size();
+        assertThat(itemCount).isAtLeast(totalCount);
+        for (int i = 0; i < itemCount; i++) {
+            clickAndWait(sDevice, itemList.get(i));
+        }
+
+        clickAndWait(sDevice, findAddButton());
+
+        final ClipData clipData = mActivity.getResult().data.getClipData();
+        assertWithMessage("Expected number of items returned to be: " + itemCount)
+                .that(clipData.getItemCount()).isEqualTo(itemCount);
+        for (int i = 0; i < itemCount; i++) {
+            final Uri uri = clipData.getItemAt(i).getUri();
+            assertPickerUriFormat(uri, mContext.getUserId());
+            assertPersistedGrant(uri, mContext.getContentResolver());
+            assertRedactedReadOnlyAccess(uri);
+            assertContainsMimeType(uri, mimeTypes);
+        }
+    }
+
+    @Test
+    public void testMimeTypeFilterPriority() throws Exception {
+        final int videoCount = 2;
+        mUriList.addAll(createMj2VideosAndGetUris(videoCount, mContext.getUserId()));
+        final int imageCount = 1;
+        mUriList.addAll(createImagesAndGetUris(imageCount, mContext.getUserId()));
+
+        Intent intent = new Intent(mAction);
+        addMultipleSelectionFlag(intent);
+        // setType has lower priority than EXTRA_MIME_TYPES filters.
+        intent.setType("image/*");
+        final String mimeType = "video/mj2";
+        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {mimeType});
+        launchPhotoPickerForIntent(intent);
+
+        // find all items
+        final List<UiObject> itemList = findItemList(-1);
+        final int itemCount = itemList.size();
+        assertThat(itemCount).isAtLeast(videoCount);
+        for (int i = 0; i < itemCount; i++) {
+            clickAndWait(sDevice, itemList.get(i));
+        }
+
+        clickAndWait(sDevice, findAddButton());
+
+        final ClipData clipData = mActivity.getResult().data.getClipData();
+        assertWithMessage("Expected number of items returned to be: " + itemCount)
+                .that(clipData.getItemCount()).isEqualTo(itemCount);
+        for (int i = 0; i < itemCount; i++) {
+            final Uri uri = clipData.getItemAt(i).getUri();
+            assertPickerUriFormat(uri, mContext.getUserId());
+            assertPersistedGrant(uri, mContext.getContentResolver());
+            assertRedactedReadOnlyAccess(uri);
+            assertMimeType(uri, mimeType);
+        }
+    }
+
+    @Test
+    public void testPickerUriFileExtensions() throws Exception {
+        // 1. Create test media items
+        mUriList.add(createSvgImage(mContext.getUserId()));
+        mUriList.add(createImageWithUnknownMimeType(mContext.getUserId()));
+        mUriList.add(createMpegVideo(mContext.getUserId()));
+        mUriList.add(createVideoWithUnknownMimeType(mContext.getUserId()));
+
+        final int expectedItemCount = mUriList.size();
+
+        final Map<String, String> mimeTypeToExpectedExtensionMap = Map.of(
+                "image/svg+xml", "svg",
+                "image/foo", "jpg",
+                "video/mpeg", "mpeg",
+                "video/foo", "mp4"
+        );
+
+        // 2. Launch Picker in multi-select mode for the test mime types
+        final Intent intent = new Intent(mAction);
+        addMultipleSelectionFlag(intent);
+        launchPhotoPickerForIntent(intent);
+
+        // 3. Add all items
+        final List<UiObject> itemList = findItemList(expectedItemCount);
+        final int itemCount = itemList.size();
+        assertWithMessage("Unexpected number of media items found in the picker ui")
+                .that(itemCount)
+                .isEqualTo(expectedItemCount);
+
+        for (UiObject item : itemList) {
+            clickAndWait(sDevice, item);
+        }
+        clickAndWait(sDevice, findAddButton());
+
+        // 4. Get the activity result data to extract the picker uris
+        final ClipData clipData = mActivity.getResult().data.getClipData();
+        assertWithMessage("Unexpected number of items returned from the picker activity")
+                .that(clipData.getItemCount())
+                .isEqualTo(itemCount);
+
+        // 5. Assert the picker uri file extension as expected for each item
+        for (int i = 0; i < itemCount; i++) {
+            final Uri uri = clipData.getItemAt(i).getUri();
+            assertExtension(uri, mimeTypeToExpectedExtensionMap);
+        }
+    }
+
     private void assertMuteButtonState(UiObject muteButton, boolean isMuted)
             throws UiObjectNotFoundException {
         // We use content description to assert the state of the mute button, there is no other way
@@ -551,26 +742,27 @@
         assertPlayerControlsAutoHide(playPauseButton, muteButton);
 
         // Click on StyledPlayerView to make the video controls visible
-        clickAndWait(findPlayerView());
+        clickAndWait(sDevice, findPlayerView());
 
         // PlayPause button is now pause button, click the button to pause the video.
-        clickAndWait(playPauseButton);
+        clickAndWait(sDevice, playPauseButton);
 
         // Wait for 1s and check that play button is not auto hidden
         assertPlayerControlsDontAutoHide(playPauseButton, muteButton);
 
         // PlayPause button is now play button, click the button to play the video.
-        clickAndWait(playPauseButton);
+        clickAndWait(sDevice, playPauseButton);
         // Check that pause button auto-hides in 1s.
         assertPlayerControlsAutoHide(playPauseButton, muteButton);
     }
 
     private void launchPreviewMultipleWithVideos(int videoCount) throws  Exception {
-        createVideos(videoCount, mContext.getUserId(), mUriList);
-        final Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
-        intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
+        mUriList.addAll(createVideosAndGetUris(videoCount, mContext.getUserId()));
+
+        Intent intent = new Intent(mAction);
         intent.setType("video/*");
-        mActivity.startActivityForResult(intent, REQUEST_CODE);
+        addMultipleSelectionFlag(intent);
+        launchPhotoPickerForIntent(intent);
 
         final List<UiObject> itemList = findItemList(videoCount);
         final int itemCount = itemList.size();
@@ -578,19 +770,23 @@
         assertThat(itemCount).isEqualTo(videoCount);
 
         for (int i = 0; i < itemCount; i++) {
-            clickAndWait(itemList.get(i));
+            clickAndWait(sDevice, itemList.get(i));
         }
 
-        clickAndWait(findViewSelectedButton());
+        clickAndWait(sDevice, findViewSelectedButton());
 
         // Wait for playback to start. This is needed in some devices where playback
         // buffering -> ready state takes around 10s.
         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();
+        sDevice.waitForIdle();
     }
 
     private void setUpAndAssertStickyPlayerControls(UiObject playerView, UiObject playPauseButton,
@@ -600,7 +796,7 @@
         // Wait for 1s or Play/Pause button to hide
         playPauseButton.waitUntilGone(1000);
         // Click on StyledPlayerView to make the video controls visible
-        clickAndWait(playerView);
+        clickAndWait(sDevice, playerView);
         assertPlayerControlsVisible(playPauseButton, muteButton);
     }
 
@@ -662,20 +858,54 @@
                 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"));
     }
 
-    private void clickAndWait(UiObject uiObject) throws Exception {
-        uiObject.click();
-        mDevice.waitForIdle();
+    private void swipeLeftAndWait() {
+        final int width = sDevice.getDisplayWidth();
+        final int height = sDevice.getDisplayHeight();
+        sDevice.swipe(15 * width / 20, height / 2, width / 20, height / 2, 10);
+        sDevice.waitForIdle();
     }
 
-    private void swipeLeftAndWait() {
-        final int width = mDevice.getDisplayWidth();
-        final int height = mDevice.getDisplayHeight();
-        mDevice.swipe(15 * width / 20, height / 2, width / 20, height / 2, 10);
-        mDevice.waitForIdle();
+    private static List<String> getTestParameters() {
+        return Arrays.asList(
+                MediaStore.ACTION_PICK_IMAGES,
+                Intent.ACTION_GET_CONTENT
+        );
+    }
+
+    private void addMultipleSelectionFlag(Intent intent) {
+        switch (intent.getAction()) {
+            case MediaStore.ACTION_PICK_IMAGES:
+                intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX,
+                        MediaStore.getPickImagesMaxLimit());
+                break;
+            case Intent.ACTION_GET_CONTENT:
+                intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+                break;
+            default:
+                // do nothing
+        }
+    }
+
+    private void launchPhotoPickerForIntent(Intent intent) throws Exception {
+        // GET_CONTENT needs to have setType
+        if (Intent.ACTION_GET_CONTENT.equals(intent.getAction()) && intent.getType() == null) {
+            intent.setType("*/*");
+            intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
+        }
+
+        mActivity.startActivityForResult(intent, REQUEST_CODE);
     }
 }
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/PickerProviderMediaGenerator.java b/tests/PhotoPicker/src/android/photopicker/cts/PickerProviderMediaGenerator.java
index 5110781..216b5de 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/PickerProviderMediaGenerator.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/PickerProviderMediaGenerator.java
@@ -432,8 +432,7 @@
             }
 
             albumId = bundle.getString(CloudMediaProviderContract.EXTRA_ALBUM_ID, null);
-            mimeType = bundle.getString(CloudMediaProviderContract.EXTRA_MIME_TYPE,
-                    null);
+            mimeType = bundle.getString(Intent.EXTRA_MIME_TYPES, null);
             sizeBytes = bundle.getLong(CloudMediaProviderContract.EXTRA_SIZE_LIMIT_BYTES, 0);
             generation = bundle.getLong(CloudMediaProviderContract.EXTRA_SYNC_GENERATION, 0);
         }
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/RemoteVideoPreviewTest.java b/tests/PhotoPicker/src/android/photopicker/cts/RemoteVideoPreviewTest.java
index 65a8e96..afd2695 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/RemoteVideoPreviewTest.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/RemoteVideoPreviewTest.java
@@ -16,17 +16,23 @@
 
 package android.photopicker.cts;
 
-import static android.os.SystemProperties.getBoolean;
+import static android.photopicker.cts.PhotoPickerCloudUtils.enableCloudMediaAndSetAllowedCloudProviders;
 import static android.photopicker.cts.PickerProviderMediaGenerator.setCloudProvider;
 import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider;
 import static android.photopicker.cts.util.PhotoPickerFilesUtils.deleteMedia;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.REGEX_PACKAGE_NAME;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
+import static android.photopicker.cts.util.PhotoPickerUiUtils.clickAndWait;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
 import static android.photopicker.cts.util.PhotoPickerUiUtils.findPreviewAddButton;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_PERMANENT_FAILURE;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE;
 import static android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -35,18 +41,19 @@
 import android.content.Intent;
 import android.graphics.PixelFormat;
 import android.net.Uri;
+import android.os.Build;
 import android.photopicker.cts.PickerProviderMediaGenerator.MediaGenerator;
 import android.photopicker.cts.cloudproviders.CloudProviderPrimary;
 import android.photopicker.cts.cloudproviders.CloudProviderPrimary.CloudMediaSurfaceControllerImpl;
 import android.provider.MediaStore;
 import android.util.Pair;
 
+import androidx.test.filters.SdkSuppress;
 import androidx.test.runner.AndroidJUnit4;
 import androidx.test.uiautomator.UiObject;
 import androidx.test.uiautomator.UiSelector;
 
 import org.junit.After;
-import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -63,6 +70,7 @@
  * End-to-end coverage for video preview controls is present in {@link PhotoPickerTest}
  */
 @RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
 public class RemoteVideoPreviewTest extends PhotoPickerBaseTest {
 
     private MediaGenerator mCloudPrimaryMediaGenerator;
@@ -84,16 +92,15 @@
     public void setUp() throws Exception {
         super.setUp();
 
-        Assume.assumeTrue(getBoolean("sys.photopicker.pickerdb.enabled", true));
-
-        mDevice.executeShellCommand("setprop sys.photopicker.remote_preview true");
-        Assume.assumeTrue(getBoolean("sys.photopicker.remote_preview", true));
-
         mCloudPrimaryMediaGenerator = PickerProviderMediaGenerator.getMediaGenerator(
                 mContext, CloudProviderPrimary.AUTHORITY);
         mCloudPrimaryMediaGenerator.resetAll();
         mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
 
+        // This is a self-instrumentation test, so both "target" package name and "own" package name
+        // should be the same (android.photopicker.cts).
+        enableCloudMediaAndSetAllowedCloudProviders(sTargetPackageName);
+
         setCloudProvider(mContext, CloudProviderPrimary.AUTHORITY);
         assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(mContext.getContentResolver(),
                 CloudProviderPrimary.AUTHORITY)).isTrue();
@@ -132,7 +139,7 @@
         // TODO(b/215187981): Add test for onMediaPause()
 
         // Exit preview mode
-        mDevice.pressBack();
+        sDevice.pressBack();
 
         // Remote Preview calls onSurfaceDestroyed, check if the id is the same (as the
         // CloudMediaProvider is only rendering to one surface id)
@@ -168,16 +175,14 @@
 
         // Remote Preview calls onSurfaceCreated with monotonically increasing surfaceIds
         final int surfaceIdForSecondVideoPreview = 1;
-        verifyAdjacentVideoSwipe(surfaceIdForFirstVideoPreview, surfaceIdForSecondVideoPreview,
-                CLOUD_ID1);
+        verifyAdjacentVideoSwipe(surfaceIdForSecondVideoPreview, CLOUD_ID1);
 
         // Swipe right in preview mode and go to first video, but the surface id will have
         // increased monotonically
         swipeRightAndWait();
 
         final int surfaceIdForThirdVideoPreview = 2;
-        verifyAdjacentVideoSwipe(surfaceIdForSecondVideoPreview, surfaceIdForThirdVideoPreview,
-                CLOUD_ID2);
+        verifyAdjacentVideoSwipe(surfaceIdForThirdVideoPreview, CLOUD_ID2);
 
         final UiObject addButton = findPreviewAddButton();
         addButton.click();
@@ -223,23 +228,56 @@
         // test the remote preview APIs
     }
 
+    @Test
+    public void testVideoPreviewProgressIndicator() throws Exception {
+        initCloudProviderWithVideo(Arrays.asList(Pair.create(null, CLOUD_ID1)));
+        launchPreviewMultiple(/* count */ 1);
+
+        // Remote Preview displays circular progress indicator when playback state is
+        // PLAYBACK_STATE_BUFFERING.
+        verifyProgressIndicatorShowsWhenBuffering(/* surfaceId */ 0);
+    }
+
+    @Test
+    public void testVideoPreviewPermanentError() throws Exception {
+        initCloudProviderWithVideo(Arrays.asList(Pair.create(null, CLOUD_ID1)));
+        launchPreviewMultiple(/* count */ 1);
+
+        // Remote Preview displays Snackbar to notify the user of an error when playback state is
+        // PLAYBACK_STATE_ERROR_PERMANENT_FAILURE.
+        verifySnackbarShowsWhenPermanentError(/* surfaceId */ 0);
+    }
+
+    @Test
+    public void testVideoPreviewRetriableError() throws Exception {
+        initCloudProviderWithVideo(Arrays.asList(Pair.create(null, CLOUD_ID1)));
+        final int surfaceId = 0;
+        launchPreviewMultiple(/* count */ 1);
+
+        // Remote Preview displays an AlertDialog to notify the user of an error when playback state
+        // is PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE.
+        verifyAlertDialogShowsWhenRetriableError(surfaceId);
+
+        // Remote Preview calls onMediaPlay when user clicks the retry button in the retriable error
+        // AlertDialog.
+        verifyAlertDialogRetry(surfaceId);
+    }
+
     /**
      * Verify surface controller interactions on swiping from one video to another.
      * Note: This test assumes that the first video is in playing state.
      *
-     * @param oldSurfaceId the Surface ID which we are swiping away from
      * @param newSurfaceId the Surface ID to which we are swiping
      * @param newMediaId   the media ID of the video we are swiping to
      */
-    private void verifyAdjacentVideoSwipe(int oldSurfaceId, int newSurfaceId, String newMediaId)
+    private void verifyAdjacentVideoSwipe(int newSurfaceId, String newMediaId)
             throws Exception {
+        // We cannot be sure of the order of onSurfaceDestroyed(oldSurfaceId) and
+        // onSurfaceCreated(newSurfaceId) calls since the Surface lifecycle is not in our control,
+        // hence we cannot verify the two calls were made using InOrder mock.
         mAssertInOrder.verify(mSurfaceControllerListener).onSurfaceCreated(eq(newSurfaceId),
                 any(), eq(newMediaId));
 
-        // The surface for the first video is destroyed when it is no longer visible on the screen
-        // (swipe is complete).
-        mAssertInOrder.verify(mSurfaceControllerListener).onSurfaceDestroyed(eq(oldSurfaceId));
-
         verifyPlaybackStartedWhenPlayerReady(newSurfaceId);
     }
 
@@ -273,6 +311,54 @@
         mAssertInOrder.verify(mSurfaceControllerListener).onMediaPlay(eq(surfaceId));
     }
 
+    private void verifyProgressIndicatorShowsWhenBuffering(int surfaceId) throws Exception {
+        CloudProviderPrimary.setPlaybackState(surfaceId, PLAYBACK_STATE_BUFFERING);
+        // Wait for photo picker to receive the event and invoke media play via binder calls.
+        MediaStore.waitForIdle(mContext.getContentResolver());
+        assertWithMessage("Expected circular progress indicator to be visible when state is "
+                + "buffering").that(findPreviewProgressIndicator().waitForExists(SHORT_TIMEOUT))
+                .isTrue();
+    }
+
+    private void verifySnackbarShowsWhenPermanentError(int surfaceId) throws Exception {
+        CloudProviderPrimary.setPlaybackState(surfaceId, PLAYBACK_STATE_ERROR_PERMANENT_FAILURE);
+        // Wait for photo picker to receive the event and invoke media play via binder calls.
+        MediaStore.waitForIdle(mContext.getContentResolver());
+        assertWithMessage("Expected snackbar to be visible when state is permanent error")
+                .that(findPreviewErrorSnackbar().waitForExists(SHORT_TIMEOUT)).isTrue();
+    }
+
+    private void verifyAlertDialogShowsWhenRetriableError(int surfaceId) throws Exception {
+        CloudProviderPrimary.setPlaybackState(surfaceId, PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE);
+        // Wait for photo picker to receive the event and invoke media play via binder calls.
+        MediaStore.waitForIdle(mContext.getContentResolver());
+
+        assertWithMessage("Expected alert dialog with title to be visible when state is retriable "
+                + "error").that(findPreviewErrorAlertDialogTitle().waitForExists(SHORT_TIMEOUT))
+                .isTrue();
+        assertWithMessage("Expected alert dialog with text body to be visible when state is "
+                + "retriable error").that(findPreviewErrorAlertDialogBody().exists()).isTrue();
+        assertWithMessage("Expected alert dialog with retry button to be visible when state is "
+                + "retriable error").that(findPreviewErrorAlertDialogRetryButton().exists())
+                .isTrue();
+        assertWithMessage("Expected alert dialog with cancel button to be visible when state is "
+                + "retriable error").that(findPreviewErrorAlertDialogCancelButton().exists())
+                .isTrue();
+    }
+
+    private void verifyAlertDialogRetry(int surfaceId) throws Exception {
+        CloudProviderPrimary.setPlaybackState(surfaceId, PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE);
+        // Wait for photo picker to receive the event and invoke media play via binder calls.
+        MediaStore.waitForIdle(mContext.getContentResolver());
+
+        assertWithMessage("Expected alert dialog with retry button to be visible when state is "
+                + "retriable error")
+                .that(findPreviewErrorAlertDialogRetryButton().waitForExists(SHORT_TIMEOUT))
+                .isTrue();
+        clickAndWait(sDevice, findPreviewErrorAlertDialogRetryButton());
+        mAssertInOrder.verify(mSurfaceControllerListener).onMediaPlay(eq(surfaceId));
+    }
+
     private void initCloudProviderWithImage(List<Pair<String, String>> mediaPairs)
             throws Exception {
         for (Pair<String, String> pair : mediaPairs) {
@@ -317,12 +403,12 @@
 
         for (final UiObject item : itemList) {
             item.click();
-            mDevice.waitForIdle();
+            sDevice.waitForIdle();
         }
 
         final UiObject viewSelectedButton = findViewSelectedButton();
         viewSelectedButton.click();
-        mDevice.waitForIdle();
+        sDevice.waitForIdle();
 
         // Wait for CloudMediaProvider binder calls to finish.
         MediaStore.waitForIdle(mContext.getContentResolver());
@@ -334,22 +420,47 @@
     }
 
     private void swipeLeftAndWait() throws Exception {
-        final int width = mDevice.getDisplayWidth();
-        final int height = mDevice.getDisplayHeight();
-        mDevice.swipe(width / 2, height / 2, width / 4, height / 2, 10);
-        mDevice.waitForIdle();
+        final int width = sDevice.getDisplayWidth();
+        final int height = sDevice.getDisplayHeight();
+        sDevice.swipe(width / 2, height / 2, width / 4, height / 2, 10);
+        sDevice.waitForIdle();
 
         // Wait for CloudMediaProvider binder calls to finish.
         MediaStore.waitForIdle(mContext.getContentResolver());
     }
 
     private void swipeRightAndWait() throws Exception {
-        final int width = mDevice.getDisplayWidth();
-        final int height = mDevice.getDisplayHeight();
-        mDevice.swipe(width / 4, height / 2, width / 2, height / 2, 10);
-        mDevice.waitForIdle();
+        final int width = sDevice.getDisplayWidth();
+        final int height = sDevice.getDisplayHeight();
+        sDevice.swipe(width / 4, height / 2, width / 2, height / 2, 10);
+        sDevice.waitForIdle();
 
         // Wait for CloudMediaProvider binder calls to finish.
         MediaStore.waitForIdle(mContext.getContentResolver());
     }
+
+    private static UiObject findPreviewProgressIndicator() {
+        return new UiObject(new UiSelector().resourceIdMatches(
+                REGEX_PACKAGE_NAME + ":id/preview_progress_indicator"));
+    }
+
+    private static UiObject findPreviewErrorAlertDialogTitle() {
+        return new UiObject(new UiSelector().text("Trouble playing video"));
+    }
+
+    private static UiObject findPreviewErrorAlertDialogBody() {
+        return new UiObject(new UiSelector().text("Check your internet connection and try again"));
+    }
+
+    private static UiObject findPreviewErrorAlertDialogRetryButton() {
+        return new UiObject(new UiSelector().textMatches("R(etry|ETRY)"));
+    }
+
+    private static UiObject findPreviewErrorAlertDialogCancelButton() {
+        return new UiObject(new UiSelector().textMatches("C(ancel|ANCEL)"));
+    }
+
+    private static UiObject findPreviewErrorSnackbar() {
+        return new UiObject(new UiSelector().text("Can't play video"));
+    }
 }
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/GetContentActivityAliasUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/GetContentActivityAliasUtils.java
new file mode 100644
index 0000000..a2b4a21
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/GetContentActivityAliasUtils.java
@@ -0,0 +1,131 @@
+/*
+ * 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.photopicker.cts.util;
+
+import static android.provider.MediaStore.ACTION_PICK_IMAGES;
+
+import android.Manifest;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+
+import androidx.annotation.NonNull;
+import androidx.test.InstrumentationRegistry;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+
+public class GetContentActivityAliasUtils {
+
+    private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
+    private static final long POLLING_SLEEP_MILLIS = 100;
+
+    private static ComponentName sComponentName = new ComponentName(
+            getPhotoPickerPackageName(),
+            "com.android.providers.media.photopicker.PhotoPickerGetContentActivity");
+
+    public static int enableAndGetOldState() throws Exception {
+        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        final PackageManager packageManager = inst.getContext().getPackageManager();
+        if (isComponentEnabledSetAsExpected(packageManager,
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED)) {
+            return PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+        }
+
+        final int currentState = packageManager.getComponentEnabledSetting(sComponentName);
+
+        updateComponentEnabledSetting(packageManager,
+                PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+
+        return currentState;
+    }
+
+    public static void restoreState(int oldState) throws Exception {
+        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        updateComponentEnabledSetting(inst.getContext().getPackageManager(), oldState);
+    }
+
+    /**
+     * Clears the package data.
+     */
+    public static void clearPackageData(String packageName) throws Exception {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                .executeShellCommand("pm clear " + packageName);
+
+        // We should ideally be listening to an effective measure to know if package data was
+        // cleared, like listening to a broadcasts or checking a value. But that information is
+        // very package private and not available.
+        Thread.sleep(500);
+    }
+
+    public static String getDocumentsUiPackageName() {
+        final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+        intent.setType("*/*");
+        return getActivityPackageNameFromIntent(intent);
+    }
+
+    private static void updateComponentEnabledSetting(PackageManager packageManager,
+            int state) throws Exception {
+        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        inst.getUiAutomation().adoptShellPermissionIdentity(
+                Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE);
+        try {
+            packageManager.setComponentEnabledSetting(sComponentName, state,
+                    PackageManager.DONT_KILL_APP);
+        } finally {
+            inst.getUiAutomation().dropShellPermissionIdentity();
+        }
+        waitForComponentToBeInExpectedState(packageManager, state);
+    }
+
+    private static void waitForComponentToBeInExpectedState(PackageManager packageManager,
+            int state) throws Exception {
+        pollForCondition(() -> isComponentEnabledSetAsExpected(packageManager, state),
+                "Timed out while waiting for component to be enabled");
+    }
+
+    private static void pollForCondition(Supplier<Boolean> condition, String errorMessage)
+            throws Exception {
+        for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
+            if (condition.get()) {
+                return;
+            }
+            Thread.sleep(POLLING_SLEEP_MILLIS);
+        }
+        throw new TimeoutException(errorMessage);
+    }
+
+    private static boolean isComponentEnabledSetAsExpected(PackageManager packageManager,
+            int state) {
+        return packageManager.getComponentEnabledSetting(sComponentName) == state;
+    }
+
+    @NonNull
+    private static String getPhotoPickerPackageName() {
+        return getActivityPackageNameFromIntent(new Intent(ACTION_PICK_IMAGES));
+    }
+
+    @NonNull
+    private static String getActivityPackageNameFromIntent(@NonNull Intent intent) {
+        final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+        final ResolveInfo ri = inst.getContext().getPackageManager().resolveActivity(intent, 0);
+        return ri.activityInfo.packageName;
+    }
+}
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java
index 3705ddd..9a39a40 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerFilesUtils.java
@@ -26,7 +26,9 @@
 import android.provider.MediaStore;
 import android.provider.cts.ProviderTestUtils;
 import android.provider.cts.media.MediaStoreUtils;
+import android.util.Pair;
 
+import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.ShellUtils;
@@ -34,6 +36,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -42,35 +45,68 @@
 public class PhotoPickerFilesUtils {
     public static final String DISPLAY_NAME_PREFIX = "ctsPhotoPicker";
 
-    public static void createImages(int count, int userId, List<Uri> uriList)
+    public static List<Uri> createImagesAndGetUris(int count, int userId)
             throws Exception {
-        createImages(count, userId, uriList, false);
+        return createImagesAndGetUris(count, userId, /* isFavorite */ false);
     }
 
-    public static void createImages(int count, int userId, List<Uri> uriList, boolean isFavorite)
+    public static List<Uri> createImagesAndGetUris(int count, int userId, boolean isFavorite)
             throws Exception {
+        List<Pair<Uri, String>> createdImagesData = createImagesAndGetUriAndPath(count, userId,
+                isFavorite);
+
+        List<Uri> uriList = new ArrayList<>();
+        for (Pair<Uri, String> createdImageData: createdImagesData) {
+            uriList.add(createdImageData.first);
+        }
+
+        return uriList;
+    }
+
+    /**
+     * Create multiple images according to the given parameters and returns the uris and filenames
+     * of the images created.
+     *
+     * @param count number of images to create
+     * @param userId user id to create images in
+     *
+     * @return list of data (uri and filename pair) about the images created
+     */
+    public static List<Pair<Uri, String>> createImagesAndGetUriAndPath(int count, int userId,
+            boolean isFavorite) throws Exception {
+        List<Pair<Uri, String>> createdImagesData = new ArrayList<>();
+
         for (int i = 0; i < count; i++) {
-            final Uri uri = createImage(userId, isFavorite);
+            Pair<Uri, String> createdImageData = createImage(userId, isFavorite);
+            createdImagesData.add(createdImageData);
+            clearMediaOwner(createdImageData.first, userId);
+        }
+        // Wait for Picker db sync to complete
+        MediaStore.waitForIdle(InstrumentationRegistry.getContext().getContentResolver());
+
+        return createdImagesData;
+    }
+
+    public static Pair<Uri, String> createImage(int userId, boolean isFavorite) throws Exception {
+        return getPermissionAndStageMedia(R.raw.lg_g4_iso_800_jpg,
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/jpeg", userId, isFavorite);
+    }
+
+    public static List<Uri> createMj2VideosAndGetUris(int count, int userId) throws Exception {
+        List<Uri> uriList = new ArrayList<>();
+        for (int i = 0; i < count; i++) {
+            final Uri uri = createMj2Video(userId);
             uriList.add(uri);
             clearMediaOwner(uri, userId);
         }
         // Wait for Picker db sync to complete
         MediaStore.waitForIdle(InstrumentationRegistry.getContext().getContentResolver());
+
+        return uriList;
     }
 
-    public static void createDNGVideos(int count, int userId, List<Uri> uriList)
-            throws Exception {
-        for (int i = 0; i < count; i++) {
-            final Uri uri = createDNGVideo(userId);
-            uriList.add(uri);
-            clearMediaOwner(uri, userId);
-        }
-        // Wait for Picker db sync to complete
-        MediaStore.waitForIdle(InstrumentationRegistry.getContext().getContentResolver());
-    }
-
-    public static void createVideos(int count, int userId, List<Uri> uriList)
-            throws Exception {
+    public static List<Uri> createVideosAndGetUris(int count, int userId) throws Exception {
+        List<Uri> uriList = new ArrayList<>();
         for (int i = 0; i < count; i++) {
             final Uri uri = createVideo(userId);
             uriList.add(uri);
@@ -78,6 +114,8 @@
         }
         // Wait for Picker db sync to complete
         MediaStore.waitForIdle(InstrumentationRegistry.getContext().getContentResolver());
+
+        return uriList;
     }
 
     public static void deleteMedia(Uri uri, Context context) throws Exception {
@@ -94,27 +132,20 @@
         ShellUtils.runShellCommand(cmd);
     }
 
-    private static Uri createDNGVideo(int userId) throws Exception {
-        final Uri uri = stageMedia(R.raw.test_video_dng,
-                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", userId);
-        return uri;
+    private static Uri createMj2Video(int userId) throws Exception {
+        return getPermissionAndStageMedia(R.raw.test_video_mj2,
+                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", userId,
+                /* isFavorite */ false).first;
     }
 
     private static Uri createVideo(int userId) throws Exception {
-        final Uri uri = stageMedia(R.raw.test_video,
-                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", userId);
-        return uri;
+        return getPermissionAndStageMedia(R.raw.test_video,
+                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", userId,
+                /* isFavorite */ false).first;
     }
 
-    private static Uri createImage(int userId, boolean isFavorite) throws Exception {
-        final Uri uri = stageMedia(R.raw.lg_g4_iso_800_jpg,
-                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/jpeg", userId, isFavorite);
-        return uri;
-    }
-
-    private static Uri stageMedia(int resId, Uri collectionUri, String mimeType, int userId,
-            boolean isFavorite) throws
-            Exception {
+    private static Pair<Uri, String> getPermissionAndStageMedia(int resId, Uri collectionUri,
+            String mimeType, int userId, boolean isFavorite) throws Exception {
         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         uiAutomation.adoptShellPermissionIdentity(
                 android.Manifest.permission.INTERACT_ACROSS_USERS,
@@ -130,14 +161,8 @@
         }
     }
 
-    private static Uri stageMedia(int resId, Uri collectionUri, String mimeType, int userId) throws
-            Exception {
-        return stageMedia(resId, collectionUri, mimeType, userId, false);
-    }
-
-    private static Uri stageMedia(int resId, Uri collectionUri, String mimeType, Context context,
-            boolean isFavorite)
-            throws IOException {
+    private static Pair<Uri, String> stageMedia(int resId, Uri collectionUri, String mimeType,
+            Context context, boolean isFavorite) throws IOException {
         final String displayName = DISPLAY_NAME_PREFIX + System.nanoTime();
         final MediaStoreUtils.PendingParams params = new MediaStoreUtils.PendingParams(
                 collectionUri, displayName, mimeType);
@@ -149,7 +174,35 @@
                  OutputStream target = session.openOutputStream()) {
                 FileUtils.copy(source, target);
             }
-            return session.publish();
+            return new Pair(session.publish(), displayName);
         }
     }
+
+    @NonNull
+    public static Uri createSvgImage(int userId) throws Exception {
+        return getPermissionAndStageMedia(R.raw.lg_g4_iso_800_svg,
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/svg+xml", userId,
+                /* isFavorite */ false).first;
+    }
+
+    @NonNull
+    public static Uri createImageWithUnknownMimeType(int userId) throws Exception {
+        return getPermissionAndStageMedia(R.raw.lg_g4_iso_800_unknown_mime_type,
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/jpeg", userId,
+                /* isFavorite */ false).first;
+    }
+
+    @NonNull
+    public static Uri createMpegVideo(int userId) throws Exception {
+        return getPermissionAndStageMedia(R.raw.test_video_mpeg,
+                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mpeg", userId,
+                /* isFavorite */ false).first;
+    }
+
+    @NonNull
+    public static Uri createVideoWithUnknownMimeType(int userId) throws Exception {
+        return getPermissionAndStageMedia(R.raw.test_video_unknown_mime_type,
+                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4", userId,
+                /* isFavorite */ false).first;
+    }
 }
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java
index d20dcd6..75dfdf4 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerUiUtils.java
@@ -20,8 +20,8 @@
 
 import android.text.format.DateUtils;
 
+import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.UiObject;
-import androidx.test.uiautomator.UiObjectNotFoundException;
 import androidx.test.uiautomator.UiScrollable;
 import androidx.test.uiautomator.UiSelector;
 
@@ -34,7 +34,8 @@
 public class PhotoPickerUiUtils {
     public static final long SHORT_TIMEOUT = 5 * DateUtils.SECOND_IN_MILLIS;
 
-    private static final long TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;
+    public static final long TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;
+
     public static final String REGEX_PACKAGE_NAME =
             "com(.google)?.android.providers.media(.module)?";
 
@@ -92,4 +93,105 @@
         return new UiObject(new UiSelector().resourceIdMatches(
                 REGEX_PACKAGE_NAME + ":id/profile_button"));
     }
+
+    public static void findAndClickBrowse(UiDevice uiDevice) throws Exception {
+        final UiObject overflowMenu = getOverflowMenuObject(uiDevice);
+        clickAndWait(uiDevice, overflowMenu);
+
+        final UiObject browseButton = new UiObject(new UiSelector().textContains("Browse"));
+        clickAndWait(uiDevice, browseButton);
+    }
+
+    public static UiObject findSettingsOverflowMenuItem(UiDevice uiDevice) throws Exception {
+        final UiObject overflowMenu = getOverflowMenuObject(uiDevice);
+        clickAndWait(uiDevice, overflowMenu);
+        return new UiObject(new UiSelector().textContains("Cloud media app"));
+    }
+
+    public static UiObject getOverflowMenuObject(UiDevice uiDevice)  {
+        // Wait for overflow menu to appear.
+        verifyOverflowMenuExists(uiDevice);
+        return new UiObject(new UiSelector().description("More options"));
+    }
+
+    public static boolean isPhotoPickerVisible() {
+        return new UiObject(new UiSelector().resourceIdMatches(
+                PhotoPickerUiUtils.REGEX_PACKAGE_NAME + ":id/bottom_sheet")).waitForExists(TIMEOUT);
+    }
+
+    public static void verifySettingsActionBarIsVisible() {
+        assertWithMessage("Timed out waiting for action bar to appear")
+                .that(new UiObject(new UiSelector()
+                        .resourceIdMatches(REGEX_PACKAGE_NAME + ":id/picker_settings_toolbar"))
+                        .waitForExists(TIMEOUT))
+                .isTrue();
+    }
+
+    public static void verifySettingsTitleIsVisible() {
+        assertWithMessage("Timed out waiting for settings page title to appear")
+                .that(new UiObject(new UiSelector()
+                        .resourceIdMatches(REGEX_PACKAGE_NAME + ":id/picker_settings_title"))
+                        .waitForExists(TIMEOUT))
+                .isTrue();
+    }
+
+    public static void verifySettingsDescriptionIsVisible() {
+        assertWithMessage("Timed out waiting for settings page description to appear")
+                .that(new UiObject(new UiSelector()
+                        .resourceIdMatches(REGEX_PACKAGE_NAME + ":id/picker_settings_description"))
+                        .waitForExists(TIMEOUT))
+                .isTrue();
+    }
+
+    public static void verifySettingsFragmentContainerExists() {
+        assertWithMessage("Timed out waiting for settings fragment container to appear")
+                .that(new UiObject(new UiSelector()
+                        .resourceIdMatches(REGEX_PACKAGE_NAME + ":id/settings_fragment_container"))
+                        .waitForExists(TIMEOUT))
+                .isTrue();
+    }
+
+    private static void verifyOverflowMenuExists(UiDevice uiDevice) {
+        assertWithMessage("Timed out waiting for overflow menu to appear")
+                .that(new UiObject(new UiSelector().description("More options"))
+                        .waitForExists(TIMEOUT))
+                .isTrue();
+    }
+
+    public static void verifySettingsActivityIsVisible() {
+        // id/settings_activity_root is the root layout in activity_photo_picker_settings.xml
+        assertWithMessage("Timed out waiting for settings activity to appear")
+                .that(new UiObject(new UiSelector()
+                .resourceIdMatches(REGEX_PACKAGE_NAME + ":id/settings_activity_root"))
+                .waitForExists(TIMEOUT))
+                .isTrue();
+    }
+
+    public static void clickAndWait(UiDevice uiDevice, UiObject uiObject) throws Exception {
+        uiObject.click();
+        uiDevice.waitForIdle();
+    }
+
+    public static String getBannerPrimaryText() throws Exception {
+        final UiObject bannerPrimaryText = findBannerPrimaryText();
+        assertWithMessage("Timed out waiting for the banner to appear")
+                .that(bannerPrimaryText.waitForExists(TIMEOUT))
+                .isTrue();
+        return bannerPrimaryText.getText();
+    }
+
+    public static UiObject findBannerPrimaryText() {
+        return new UiObject(new UiSelector().resourceIdMatches(
+                REGEX_PACKAGE_NAME + ":id/banner_primary_text"));
+    }
+
+    public static UiObject findBannerDismissButton() {
+        return new UiObject(new UiSelector().resourceIdMatches(
+                REGEX_PACKAGE_NAME + ":id/dismiss_button"));
+    }
+
+    public static UiObject findBannerActionButton() {
+        return new UiObject(new UiSelector().resourceIdMatches(
+                REGEX_PACKAGE_NAME + ":id/action_button"));
+    }
 }
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerAssertionsUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/ResultsAssertionsUtils.java
similarity index 75%
rename from tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerAssertionsUtils.java
rename to tests/PhotoPicker/src/android/photopicker/cts/util/ResultsAssertionsUtils.java
index 9eb7d59..2f34677 100644
--- a/tests/PhotoPicker/src/android/photopicker/cts/util/PhotoPickerAssertionsUtils.java
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/ResultsAssertionsUtils.java
@@ -33,6 +33,7 @@
 import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
 
+import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
 
 import java.io.ByteArrayOutputStream;
@@ -44,11 +45,12 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 /**
- * Photo Picker Utility methods for test assertions.
+ * Photo Picker Utility methods for PhotoPicker result assertions.
  */
-public class PhotoPickerAssertionsUtils {
+public class ResultsAssertionsUtils {
     private static final String TAG = "PhotoPickerTestAssertions";
 
     public static void assertPickerUriFormat(Uri uri, int expectedUserId) {
@@ -80,6 +82,12 @@
         assertThat(resultMimeType).isEqualTo(expectedMimeType);
     }
 
+    public static void assertContainsMimeType(Uri uri, String[] expectedMimeTypes) {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final String resultMimeType = context.getContentResolver().getType(uri);
+        assertThat(Arrays.asList(expectedMimeTypes).contains(resultMimeType)).isTrue();
+    }
+
     public static void assertRedactedReadOnlyAccess(Uri uri) throws Exception {
         assertThat(uri).isNotNull();
         final String[] projection = new String[]{ PickerMediaColumns.MIME_TYPE };
@@ -101,11 +109,39 @@
         }
     }
 
+    public static void assertExtension(@NonNull Uri uri,
+            @NonNull Map<String, String> mimeTypeToExpectedExtensionMap) {
+        assertThat(uri).isNotNull();
+
+        final ContentResolver resolver =
+                InstrumentationRegistry.getTargetContext().getContentResolver();
+        final String[] projection =
+                new String[]{ PickerMediaColumns.MIME_TYPE, PickerMediaColumns.DISPLAY_NAME };
+
+        try (Cursor c = resolver.query(
+                uri, projection, /* queryArgs */ null, /* cancellationSignal */ null)) {
+            assertThat(c).isNotNull();
+            assertThat(c.moveToFirst()).isTrue();
+
+            final String mimeType = c.getString(c.getColumnIndex(PickerMediaColumns.MIME_TYPE));
+            final String expectedExtension = mimeTypeToExpectedExtensionMap.get(mimeType);
+
+            final String displayName =
+                    c.getString(c.getColumnIndex(PickerMediaColumns.DISPLAY_NAME));
+            final String[] displayNameParts = displayName.split("\\.");
+            final String resultExtension = displayNameParts[displayNameParts.length - 1];
+
+            assertWithMessage("Unexpected picker file extension")
+                    .that(resultExtension)
+                    .isEqualTo(expectedExtension);
+        }
+    }
+
     private static void assertVideoRedactedReadOnlyAccess(Uri uri, ContentResolver resolver)
             throws Exception {
         // The location is redacted
         // TODO(b/201505595): Make this method work for test_video.mp4. Currently it works only for
-        //  test_video_dng.mp4
+        //  test_video_mj2.mp4
         try (InputStream in = resolver.openInputStream(uri);
                 ByteArrayOutputStream out = new ByteArrayOutputStream()) {
             FileUtils.copy(in, out);
@@ -120,11 +156,7 @@
                     .that(xmp.contains("13166/7763")).isTrue();
         }
 
-        // assert no write access
-        try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "w")) {
-            fail("Does not grant write access to uri " + uri.toString());
-        } catch (SecurityException | FileNotFoundException expected) {
-        }
+        assertNoWriteAccess(uri, resolver);
     }
 
     private static void assertImageRedactedReadOnlyAccess(Uri uri, ContentResolver resolver)
@@ -153,12 +185,7 @@
                 assertImageExifRedacted(is);
             }
 
-            // Assert no write access
-            try (ParcelFileDescriptor pfd =
-                    ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)) {
-                fail("Does not grant write access to file " + file);
-            } catch (IOException e) {
-            }
+            assertNoWriteAccess(uri, resolver);
         }
     }
 
@@ -179,4 +206,19 @@
         assertWithMessage("Redacted non-location XMP")
                 .that(xmp.contains("LensDefaults")).isTrue();
     }
+
+    public static void assertReadOnlyAccess(Uri uri, ContentResolver resolver) throws Exception {
+        try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r")) {
+            assertThat(pfd).isNotNull();
+        }
+
+        assertNoWriteAccess(uri, resolver);
+    }
+
+    private static void assertNoWriteAccess(Uri uri, ContentResolver resolver) throws Exception {
+        try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "w")) {
+            fail("Does not grant write access to uri " + uri.toString());
+        } catch (SecurityException | FileNotFoundException expected) {
+        }
+    }
 }
diff --git a/tests/PhotoPicker/src/android/photopicker/cts/util/UiAssertionUtils.java b/tests/PhotoPicker/src/android/photopicker/cts/util/UiAssertionUtils.java
new file mode 100644
index 0000000..7caeb6a
--- /dev/null
+++ b/tests/PhotoPicker/src/android/photopicker/cts/util/UiAssertionUtils.java
@@ -0,0 +1,49 @@
+/*
+ * 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.photopicker.cts.util;
+
+import static android.photopicker.cts.util.PhotoPickerUiUtils.SHORT_TIMEOUT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.uiautomator.UiObject;
+import androidx.test.uiautomator.UiSelector;
+
+/**
+ * Photo Picker Utility methods for PhotoPicker UI assertions.
+ */
+public class UiAssertionUtils {
+    /**
+     * Verifies PhotoPicker UI is shown.
+     */
+    public static void assertThatShowsPickerUi() {
+        // Assert that Bottom Sheet is shown
+        // Add a short timeout wait for PhotoPicker to show
+        assertThat(new UiObject(new UiSelector().resourceIdMatches(
+                PhotoPickerUiUtils.REGEX_PACKAGE_NAME + ":id/bottom_sheet"))
+                .waitForExists(SHORT_TIMEOUT)).isTrue();
+
+        // Assert that privacy text is shown
+        assertThat(new UiObject(new UiSelector().resourceIdMatches(
+                PhotoPickerUiUtils.REGEX_PACKAGE_NAME + ":id/privacy_text"))
+                .exists()).isTrue();
+
+        // Assert that "Photos" and "Albums" headers are shown.
+        assertThat(new UiObject(new UiSelector().text("Photos")).exists()).isTrue();
+        assertThat(new UiObject(new UiSelector().text("Albums")).exists()).isTrue();
+    }
+}
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/app/AppSearchSessionCtsTestBase.java b/tests/appsearch/src/com/android/cts/appsearch/external/app/AppSearchSessionCtsTestBase.java
index 70b68b4..affdaa8 100644
--- a/tests/appsearch/src/com/android/cts/appsearch/external/app/AppSearchSessionCtsTestBase.java
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/app/AppSearchSessionCtsTestBase.java
@@ -676,6 +676,7 @@
                                         .build())
                         .build();
         mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
         // Creates a large batch of Documents, since we have max document size in Framework which is
         // 512KiB, we will create 1KiB * 4000 docs = 4MiB total size > 1MiB binder transaction limit
         char[] chars = new char[1024]; // 1KiB
@@ -3851,4 +3852,104 @@
         documents = convertSearchResultsToDocuments(searchResults);
         assertThat(documents).containsExactly(inEmail1);
     }
+
+    @Test
+    public void testSetSchemaWithIncompatibleNestedSchema() throws Exception {
+        // 1. Set the original schema. This should succeed without any problems.
+        AppSearchSchema originalNestedSchema =
+                new AppSearchSchema.Builder("TypeA")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("prop1")
+                                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                        .build())
+                        .build();
+        SetSchemaRequest originalRequest =
+                new SetSchemaRequest.Builder().addSchemas(originalNestedSchema).build();
+        mDb1.setSchemaAsync(originalRequest).get();
+
+        // 2. Set a new schema with a new type that refers to "TypeA" and an incompatible change to
+        // "TypeA". This should fail.
+        AppSearchSchema newNestedSchema =
+                new AppSearchSchema.Builder("TypeA")
+                        .addProperty(
+                                new StringPropertyConfig.Builder("prop1")
+                                        .setCardinality(PropertyConfig.CARDINALITY_REQUIRED)
+                                        .build())
+                        .build();
+        AppSearchSchema newSchema =
+                new AppSearchSchema.Builder("TypeB")
+                        .addProperty(
+                                new AppSearchSchema.DocumentPropertyConfig.Builder("prop2", "TypeA")
+                                        .build())
+                        .build();
+        final SetSchemaRequest newRequest =
+                new SetSchemaRequest.Builder().addSchemas(newNestedSchema, newSchema).build();
+        Throwable throwable =
+                assertThrows(ExecutionException.class, () -> mDb1.setSchemaAsync(newRequest).get())
+                        .getCause();
+        assertThat(throwable).isInstanceOf(AppSearchException.class);
+        AppSearchException exception = (AppSearchException) throwable;
+        assertThat(exception.getResultCode()).isEqualTo(RESULT_INVALID_SCHEMA);
+        assertThat(exception).hasMessageThat().contains("Schema is incompatible.");
+        assertThat(exception).hasMessageThat().contains("Incompatible types: {TypeA}");
+
+        // 3. Now set that same set of schemas but with forceOverride=true. This should succeed.
+        SetSchemaRequest newRequestForced =
+                new SetSchemaRequest.Builder()
+                        .addSchemas(newNestedSchema, newSchema)
+                        .setForceOverride(true)
+                        .build();
+        mDb1.setSchemaAsync(newRequestForced).get();
+    }
+
+    @Test
+    public void testEmojiSnippet() throws Exception {
+        // Schema registration
+        mDb1.setSchemaAsync(
+                        new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build())
+                .get();
+
+        // String:     "Luca Brasi sleeps with the 🐟🐟🐟."
+        //              ^    ^     ^      ^    ^   ^ ^  ^ ^
+        // UTF8 idx:    0    5     11     18   23 27 3135 39
+        // UTF16 idx:   0    5     11     18   23 27 2931 33
+        // Breaks into segments: "Luca", "Brasi", "sleeps", "with", "the", "🐟", "🐟"
+        // and "🐟".
+        // Index a document to instance 1.
+        String sicilianMessage = "Luca Brasi sleeps with the 🐟🐟🐟.";
+        AppSearchEmail inEmail1 =
+                new AppSearchEmail.Builder("namespace", "uri1").setBody(sicilianMessage).build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail1).build()));
+
+        AppSearchEmail inEmail2 =
+                new AppSearchEmail.Builder("namespace", "uri2")
+                        .setBody("Some other content.")
+                        .build();
+        checkIsBatchResultSuccess(
+                mDb1.putAsync(
+                        new PutDocumentsRequest.Builder().addGenericDocuments(inEmail2).build()));
+
+        // Query for "🐟"
+        SearchResultsShim searchResults =
+                mDb1.search(
+                        "🐟",
+                        new SearchSpec.Builder()
+                                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                                .setSnippetCount(1)
+                                .setSnippetCountPerProperty(1)
+                                .build());
+        List<SearchResult> page = searchResults.getNextPageAsync().get();
+        assertThat(page).hasSize(1);
+        assertThat(page.get(0).getGenericDocument()).isEqualTo(inEmail1);
+        List<SearchResult.MatchInfo> matches = page.get(0).getMatchInfos();
+        assertThat(matches).hasSize(1);
+        assertThat(matches.get(0).getPropertyPath()).isEqualTo("body");
+        assertThat(matches.get(0).getFullText()).isEqualTo(sicilianMessage);
+        assertThat(matches.get(0).getExactMatch()).isEqualTo("🐟");
+        if (mDb1.getFeatures().isFeatureSupported(Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH)) {
+            assertThat(matches.get(0).getSubmatch()).isEqualTo("🐟");
+        }
+    }
 }
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/app/GenericDocumentCtsTest.java b/tests/appsearch/src/com/android/cts/appsearch/external/app/GenericDocumentCtsTest.java
index 5dd2eac..0bfa693 100644
--- a/tests/appsearch/src/com/android/cts/appsearch/external/app/GenericDocumentCtsTest.java
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/app/GenericDocumentCtsTest.java
@@ -576,6 +576,30 @@
     }
 
     @Test
+    public void testNestedProperties_buildBlankPaths() {
+        Exception e =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                                        .setPropertyString("", "foo"));
+        assertThat(e.getMessage()).isEqualTo("Property name cannot be blank.");
+
+        e =
+                assertThrows(
+                        IllegalArgumentException.class,
+                        () ->
+                                new GenericDocument.Builder<>("namespace", "id1", "schema1")
+                                        .setPropertyDocument(
+                                                "propDoc",
+                                                new GenericDocument.Builder<>(
+                                                                "namespace", "id2", "schema1")
+                                                        .setPropertyString("", "Bat", "Hawk")
+                                                        .build()));
+        assertThat(e.getMessage()).isEqualTo("Property name cannot be blank.");
+    }
+
+    @Test
     public void testNestedProperties_invalidPaths() {
         GenericDocument doc =
                 new GenericDocument.Builder<>("namespace", "id1", "schema1")
@@ -592,25 +616,28 @@
                                         .build())
                         .build();
 
-        // Some paths are invalid because they don't apply to the given document --- these should
+        // These paths are invalid because they don't apply to the given document --- these should
         // return null. It's not the querier's fault.
         assertThat(doc.getPropertyStringArray("propString.propInts")).isNull();
         assertThat(doc.getPropertyStringArray("propDocs.propFoo")).isNull();
         assertThat(doc.getPropertyStringArray("propDocs.propNestedString.propFoo")).isNull();
+    }
 
-        // Some paths are invalid because they are malformed. These throw an exception --- the
-        // querier shouldn't provide such paths.
-        assertThrows(
-                IllegalArgumentException.class, () -> doc.getPropertyStringArray("propString[0"));
-        assertThrows(
-                IllegalArgumentException.class, () -> doc.getPropertyStringArray("propString[0.]"));
-        assertThrows(
-                IllegalArgumentException.class,
-                () -> doc.getPropertyStringArray("propString[banana]"));
-        assertThrows(
-                IllegalArgumentException.class, () -> doc.getPropertyStringArray("propString[-1]"));
-        assertThrows(
-                IllegalArgumentException.class, () -> doc.getPropertyStringArray("propDocs[0]cat"));
+    @Test
+    public void testNestedProperties_arrayTypesInvalidPath() {
+        GenericDocument doc = new GenericDocument.Builder<>("namespace", "id1", "schema1").build();
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyString("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyDocument("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyBoolean("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyDouble("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyLong("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyBytes("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyStringArray("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyDocumentArray("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyBooleanArray("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyDoubleArray("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyLongArray("."));
+        assertThrows(IllegalArgumentException.class, () -> doc.getPropertyBytesArray("."));
     }
 
     @Test
diff --git a/tests/appsearch/src/com/android/cts/appsearch/external/app/GlobalSearchSessionCtsTestBase.java b/tests/appsearch/src/com/android/cts/appsearch/external/app/GlobalSearchSessionCtsTestBase.java
index 71822ca..47337ca 100644
--- a/tests/appsearch/src/com/android/cts/appsearch/external/app/GlobalSearchSessionCtsTestBase.java
+++ b/tests/appsearch/src/com/android/cts/appsearch/external/app/GlobalSearchSessionCtsTestBase.java
@@ -1641,6 +1641,11 @@
 
     @Test
     public void testAddObserver_schemaChange_added() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                        .isFeatureSupported(
+                                Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
         // Register an observer
         TestObserverCallback observer = new TestObserverCallback();
         mGlobalSearchSession.registerObserverCallback(
@@ -1688,6 +1693,11 @@
 
     @Test
     public void testAddObserver_schemaChange_removed() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                        .isFeatureSupported(
+                                Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
         // Add a schema type
         mDb1.setSchemaAsync(
                         new SetSchemaRequest.Builder()
@@ -1723,6 +1733,11 @@
 
     @Test
     public void testAddObserver_schemaChange_contents() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                        .isFeatureSupported(
+                                Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
         // Add a schema
         mDb1.setSchemaAsync(
                         new SetSchemaRequest.Builder()
@@ -1794,6 +1809,11 @@
 
     @Test
     public void testAddObserver_schemaChange_contents_skipBySpec() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                        .isFeatureSupported(
+                                Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
         // Add a schema
         mDb1.setSchemaAsync(
                         new SetSchemaRequest.Builder()
@@ -1862,6 +1882,11 @@
 
     @Test
     public void testRegisterObserver_schemaMigration() throws Exception {
+        assumeTrue(
+                mDb1.getFeatures()
+                        .isFeatureSupported(
+                                Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK));
+
         // Add a schema with two types
         mDb1.setSchemaAsync(
                         new SetSchemaRequest.Builder()
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dialog/LoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dialog/LoginActivityTest.java
index 18dd442..8ebf2f3 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/dialog/LoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/dialog/LoginActivityTest.java
@@ -37,9 +37,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertThrows;
-
-import android.autofillservice.cts.R;
 import android.autofillservice.cts.activities.FieldsNoPasswordActivity;
 import android.autofillservice.cts.activities.LoginActivity;
 import android.autofillservice.cts.commontests.AutoFillServiceTestCase;
@@ -50,11 +47,7 @@
 import android.content.Intent;
 import android.service.autofill.FillEventHistory;
 import android.support.test.uiautomator.UiObject2;
-import android.graphics.drawable.Icon;
 import android.view.View;
-import android.widget.RemoteViews;
-
-import com.android.compatibility.common.util.RetryableException;
 
 import org.junit.Test;
 
@@ -694,51 +687,6 @@
         assertMockImeStatus(activity, true);
     }
 
-    @Test
-    public void remoteViews_doesNotSpillAcrossUsers() throws Exception {
-        enableFillDialogFeature(sContext);
-        enableService();
-
-        RemoteViews header = createPresentation("Dialog Header");
-        RemoteViews dialogRv = createPresentation("Dialog Presentation");
-        // bad url, should not be displayed
-        header.setImageViewIcon(R.id.icon, Icon.createWithContentUri(
-                "content://1000@com.android.contacts/display_photo/1"));
-        dialogRv.setImageViewIcon(R.id.icon, Icon.createWithContentUri(
-                "content://1000@com.android.contacts/display_photo/1"));
-
-        // Set response with a dataset
-        final CannedFillResponse.Builder builder = new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                    .setField(ID_USERNAME, "dude")
-                    .setField(ID_PASSWORD, "sweet")
-                    .setPresentation(createPresentation("Dropdown Presentation"))
-                    .setDialogPresentation(dialogRv)
-                    .build())
-                .setDialogHeader(header)
-                .setDialogTriggerIds(ID_PASSWORD);
-        sReplier.addResponse(builder.build());
-
-        // Start activity and autofill
-        LoginActivity activity = startLoginActivity();
-        mUiBot.waitForIdleSync();
-
-        sReplier.getNextFillRequest();
-        mUiBot.waitForIdleSync();
-
-        // Click on password field to trigger fill dialog
-        mUiBot.selectByRelativeId(ID_PASSWORD);
-        mUiBot.waitForIdleSync();
-
-        // Asserts that the header is not shown
-        assertThrows(RetryableException.class,
-                () -> mUiBot.findFillDialogHeaderPicker());
-
-        // Asserts that the
-        assertThrows(RetryableException.class,
-                () -> mUiBot.findFillDialogDatasetPicker());
-    }
-
     private FieldsNoPasswordActivity startNoPasswordActivity() throws Exception {
         final Intent intent = new Intent(mContext, FieldsNoPasswordActivity.class)
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
diff --git a/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java b/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java
index 5ed569c..175b4f8 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/dropdown/LoginActivityTest.java
@@ -71,8 +71,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
-import static org.junit.Assert.assertThrows;
-
 import android.app.PendingIntent;
 import android.app.assist.AssistStructure.ViewNode;
 import android.autofillservice.cts.R;
@@ -101,7 +99,6 @@
 import android.content.IntentSender;
 import android.graphics.Color;
 import android.graphics.Rect;
-import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
@@ -1388,46 +1385,6 @@
         mActivity.assertAutoFilled();
     }
 
-    @Test
-    public void remoteViews_doesNotSpillAcrossUsers() throws Exception {
-        // Set service.
-        enableService();
-
-
-        RemoteViews firstRv = createPresentation("hello");
-        RemoteViews secondRv = createPresentation("world");
-
-        // bad url, should not be displayed
-        firstRv.setImageViewIcon(R.id.icon,
-                Icon.createWithContentUri("content://1000@com.android.contacts/display_photo/1"));
-        secondRv.setImageViewIcon(R.id.icon,
-                Icon.createWithContentUri("content://1000@com.android.contacts/display_photo/1"));
-
-        // Set expectations.
-        sReplier.addResponse(new CannedFillResponse.Builder()
-                .addDataset(new CannedDataset.Builder()
-                    .setField(ID_USERNAME, "dude", firstRv)
-                    .setField(ID_PASSWORD, "sweet", secondRv)
-                    .build())
-                .setHeader(firstRv)
-                .setFooter(secondRv)
-                .build());
-
-        mActivity.expectAutoFill("dude", "sweet");
-
-        // Trigger auto-fill.
-        mActivity.onUsername(View::requestFocus);
-        sReplier.getNextFillRequest();
-
-        // Assert that the dataset is not shown
-        assertThrows(RetryableException.class,
-                () -> mUiBot.assertDatasets("The Dude"));
-
-        // Assert that header/footer is not shown
-        assertThrows(RetryableException.class,
-                () -> mUiBot.assertDatasetsWithBorders("hello", "world", "The Dude"));
-    }
-
     /**
      * Tests the scenario where the service uses custom remote views for different fields (username
      * and password), but each dataset has a different number of fields.
diff --git a/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java b/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java
index 7110509..50ef382 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/testcore/UiBot.java
@@ -1390,11 +1390,11 @@
         return waitForObject(FILL_DIALOG_SELECTOR, UI_DATASET_PICKER_TIMEOUT);
     }
 
-    public UiObject2 findFillDialogDatasetPicker() throws Exception {
+    private UiObject2 findFillDialogDatasetPicker() throws Exception {
         return waitForObject(FILL_DIALOG_DATASET_SELECTOR, UI_DATASET_PICKER_TIMEOUT);
     }
 
-    public UiObject2 findFillDialogHeaderPicker() throws Exception {
+    private UiObject2 findFillDialogHeaderPicker() throws Exception {
         return waitForObject(FILL_DIALOG_HEADER_SELECTOR, UI_DATASET_PICKER_TIMEOUT);
     }
 
diff --git a/tests/backup/AndroidTest.xml b/tests/backup/AndroidTest.xml
index 0e84662..d449870 100644
--- a/tests/backup/AndroidTest.xml
+++ b/tests/backup/AndroidTest.xml
@@ -20,6 +20,7 @@
     <!-- Backup of instant apps is not supported. -->
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.permission.apex" />
     <!-- Run module in system user because backup tests are not fully supported in secondary user.
      For devices running on secondary user, such as automotive devices, these tests will fail.
      When backup tests are fully functional for secondary users:
diff --git a/tests/backup/TEST_MAPPING b/tests/backup/TEST_MAPPING
index 4e5beb0..1e1dcdb 100644
--- a/tests/backup/TEST_MAPPING
+++ b/tests/backup/TEST_MAPPING
@@ -3,5 +3,15 @@
     {
       "name": "CtsBackupTestCases"
     }
+  ],
+  "mainline-presubmit": [
+    {
+      "name": "CtsBackupTestCases[com.google.android.permission.apex]",
+      "options": [
+        {
+          "include-filter": "android.backup.cts.PermissionTest"
+        }
+      ]
+    }
   ]
 }
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/media/AndroidTest.xml b/tests/media/AndroidTest.xml
index 588ddf8..ef5dbeb 100644
--- a/tests/media/AndroidTest.xml
+++ b/tests/media/AndroidTest.xml
@@ -19,6 +19,14 @@
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <option name="force-skip-system-props" value="true" /> <!-- avoid restarting device -->
+        <option name="set-test-harness" value="false" />
+        <option name="screen-always-on" value="on" />
+        <option name="screen-adaptive-brightness" value="off" />
+        <option name="disable-audio" value="false"/>
+        <option name="screen-saver" value="off"/>
+    </target_preparer>
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DynamicConfigPusher">
         <option name="target" value="host" />
         <option name="config-filename" value="CtsMediaV2TestCases" />
diff --git a/tests/media/src/android/mediav2/cts/CodecTestBase.java b/tests/media/src/android/mediav2/cts/CodecTestBase.java
index 6e51cb5..9880966 100644
--- a/tests/media/src/android/mediav2/cts/CodecTestBase.java
+++ b/tests/media/src/android/mediav2/cts/CodecTestBase.java
@@ -30,7 +30,9 @@
 import android.media.MediaCodecList;
 import android.media.MediaExtractor;
 import android.media.MediaFormat;
+import android.media.MediaMuxer;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.PersistableBundle;
 import android.os.SystemProperties;
 import android.util.Log;
@@ -591,19 +593,17 @@
 abstract class CodecTestBase {
     public static final boolean IS_Q = ApiLevelUtil.getApiLevel() == Build.VERSION_CODES.Q;
     public static final boolean IS_AT_LEAST_R = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R);
-    // Checking for CODENAME helps in cases when build version on the development branch isn't
-    // updated yet but CODENAME is updated.
     public static final boolean IS_AT_LEAST_T =
-            ApiLevelUtil.isAtLeast(Build.VERSION_CODES.TIRAMISU) ||
-                    ApiLevelUtil.codenameEquals("Tiramisu");
-    // TODO (b/223868241) Update the following to check for Build.VERSION_CODES.TIRAMISU once
-    // TIRAMISU is set correctly
+            ApiLevelUtil.isAtLeast(Build.VERSION_CODES.TIRAMISU);
+    //TODO(b/248315681) Remove codenameEquals() check once devices return correct version for U
+    public static final boolean IS_AT_LEAST_U = ApiLevelUtil.isAfter(Build.VERSION_CODES.TIRAMISU)
+            || ApiLevelUtil.codenameEquals("UpsideDownCake");
     public static final boolean FIRST_SDK_IS_AT_LEAST_T =
-            ApiLevelUtil.isFirstApiAfter(Build.VERSION_CODES.S_V2);
+            ApiLevelUtil.isFirstApiAtLeast(Build.VERSION_CODES.TIRAMISU);
     public static final boolean VNDK_IS_AT_LEAST_T =
-            SystemProperties.getInt("ro.vndk.version", 0) > Build.VERSION_CODES.S_V2;
+            SystemProperties.getInt("ro.vndk.version", 0) >= Build.VERSION_CODES.TIRAMISU;
     public static final boolean BOARD_SDK_IS_AT_LEAST_T =
-            SystemProperties.getInt("ro.board.api_level", 0) > Build.VERSION_CODES.S_V2;
+            SystemProperties.getInt("ro.board.api_level", 0) >= Build.VERSION_CODES.TIRAMISU;
     public static final boolean IS_HDR_EDITING_SUPPORTED = isHDREditingSupported();
     private static final String LOG_TAG = CodecTestBase.class.getSimpleName();
     enum SupportClass {
@@ -612,30 +612,14 @@
         CODEC_DEFAULT, // Default codec must support
         CODEC_OPTIONAL // Codec support is optional
     }
+
+    static final ArrayList<String> HDR_INFO_IN_BITSTREAM_CODECS = new ArrayList<>();
     static final String HDR_STATIC_INFO =
-            "00 d0 84 80 3e c2 33 c4 86 4c 1d b8 0b 13 3d 42 40 e8 03 64 00 e8 03 2c 01";
-    static final String[] HDR_DYNAMIC_INFO = new String[]{
-            "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
-            "0a 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
-            "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
-            "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 00",
-
-            "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
-            "0a 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
-            "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
-            "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 00",
-
-            "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
-            "0e 80 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
-            "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
-            "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 00",
-
-            "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
-            "0e 80 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
-            "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
-            "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 00",
-    };
-    boolean mTestDynamicMetadata = false;
+            "00 d0 84 80 3e c2 33 c4 86 4c 1d b8 0b 13 3d 42 40 a0 0f 32 00 10 27 df 0d";
+    static final String HDR_STATIC_INCORRECT_INFO =
+            "00 d0 84 80 3e c2 33 c4 86 10 27 d0 07 13 3d 42 40 a0 0f 32 00 10 27 df 0d";
+    static final HashMap<Integer, String> HDR_DYNAMIC_INFO = new HashMap<>();
+    static final HashMap<Integer, String> HDR_DYNAMIC_INCORRECT_INFO = new HashMap<>();
     static final String CODEC_PREFIX_KEY = "codec-prefix";
     static final String MEDIA_TYPE_PREFIX_KEY = "media-type-prefix";
     static final String MIME_SEL_KEY = "mime-sel";
@@ -791,6 +775,45 @@
         mProfileMap.put(MediaFormat.MIMETYPE_VIDEO_VP9, VP9_PROFILES);
         mProfileMap.put(MediaFormat.MIMETYPE_VIDEO_AV1, AV1_PROFILES);
         mProfileMap.put(MediaFormat.MIMETYPE_AUDIO_AAC, AAC_PROFILES);
+
+        HDR_INFO_IN_BITSTREAM_CODECS.add(MediaFormat.MIMETYPE_VIDEO_AV1);
+        HDR_INFO_IN_BITSTREAM_CODECS.add(MediaFormat.MIMETYPE_VIDEO_AVC);
+        HDR_INFO_IN_BITSTREAM_CODECS.add(MediaFormat.MIMETYPE_VIDEO_HEVC);
+
+        HDR_DYNAMIC_INFO.put(0, "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+                "0a 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
+                "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
+                "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 00");
+        HDR_DYNAMIC_INFO.put(4, "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+                "0a 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
+                "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
+                "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 00");
+        HDR_DYNAMIC_INFO.put(12, "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+                "0e 80 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
+                "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
+                "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 00");
+        HDR_DYNAMIC_INFO.put(22, "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+                "0e 80 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
+                "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
+                "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 00");
+
+
+        HDR_DYNAMIC_INCORRECT_INFO.put(0, "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+                "0a 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
+                "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
+                "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 00");
+        HDR_DYNAMIC_INCORRECT_INFO.put(4, "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+                "0a 00 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
+                "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
+                "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 01");
+        HDR_DYNAMIC_INCORRECT_INFO.put(12, "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+                "0e 80 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
+                "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
+                "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 02");
+        HDR_DYNAMIC_INCORRECT_INFO.put(22, "b5 00 3c 00 01 04 00 40  00 0c 80 4e 20 27 10 00" +
+                "0e 80 00 24 08 00 00 28  00 00 50 00 28 c8 00 c9" +
+                "90 02 aa 58 05 ca d0 0c  0a f8 16 83 18 9c 18 00" +
+                "40 78 13 64 d5 7c 2e 2c  c3 59 de 79 6e c3 c2 03");
     }
 
     static int[] combine(int[] first, int[] second) {
@@ -1369,6 +1392,12 @@
         return Arrays.copyOfRange(tempArray, 0, i);
     }
 
+    void insertHdrDynamicInfo(byte[] info) {
+        final Bundle params = new Bundle();
+        params.putByteArray(MediaFormat.KEY_HDR10_PLUS_INFO, info);
+        mCodec.setParameters(params);
+    }
+
     boolean isFormatSimilar(MediaFormat inpFormat, MediaFormat outFormat) {
         if (inpFormat == null || outFormat == null) return false;
         String inpMime = inpFormat.getString(MediaFormat.KEY_MIME);
@@ -1427,41 +1456,26 @@
         }
     }
 
-    void validateHDRStaticMetaData(MediaFormat fmt, ByteBuffer hdrStaticRef) {
-        ByteBuffer hdrStaticInfo = fmt.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, null);
-        assertNotNull("No HDR static metadata present in format : " + fmt, hdrStaticInfo);
-        if (!hdrStaticRef.equals(hdrStaticInfo)) {
+    void validateHDRInfo(MediaFormat fmt, String hdrInfoKey, ByteBuffer hdrInfoRef) {
+        ByteBuffer hdrInfo = fmt.getByteBuffer(hdrInfoKey, null);
+        assertNotNull("No " + hdrInfoKey + " present in format : " + fmt, hdrInfo);
+        if (!hdrInfoRef.equals(hdrInfo)) {
             StringBuilder refString = new StringBuilder("");
             StringBuilder testString = new StringBuilder("");
-            byte[] ref = new byte[hdrStaticRef.capacity()];
-            hdrStaticRef.get(ref);
-            byte[] test = new byte[hdrStaticInfo.capacity()];
-            hdrStaticInfo.get(test);
-            for (int i = 0; i < Math.min(ref.length, test.length); i++) {
+            byte[] ref = new byte[hdrInfoRef.capacity()];
+            hdrInfoRef.get(ref);
+            hdrInfoRef.rewind();
+            byte[] test = new byte[hdrInfo.capacity()];
+            hdrInfo.get(test);
+            hdrInfo.rewind();
+            for (int i = 0; i < ref.length; i++) {
                 refString.append(String.format("%2x ", ref[i]));
+            }
+            for (int i = 0; i < test.length; i++) {
                 testString.append(String.format("%2x ", test[i]));
             }
-            fail("hdr static info mismatch" + "\n" + "ref static info : " + refString + "\n" +
-                    "test static info : " + testString);
-        }
-    }
-
-    void validateHDRDynamicMetaData(MediaFormat fmt, ByteBuffer hdrDynamicRef) {
-        ByteBuffer hdrDynamicInfo = fmt.getByteBuffer(MediaFormat.KEY_HDR10_PLUS_INFO, null);
-        assertNotNull("No HDR dynamic metadata present in format : " + fmt, hdrDynamicInfo);
-        if (!hdrDynamicRef.equals(hdrDynamicInfo)) {
-            StringBuilder refString = new StringBuilder("");
-            StringBuilder testString = new StringBuilder("");
-            byte[] ref = new byte[hdrDynamicRef.capacity()];
-            hdrDynamicRef.get(ref);
-            byte[] test = new byte[hdrDynamicInfo.capacity()];
-            hdrDynamicInfo.get(test);
-            for (int i = 0; i < Math.min(ref.length, test.length); i++) {
-                refString.append(String.format("%2x ", ref[i]));
-                testString.append(String.format("%2x ", test[i]));
-            }
-            fail("hdr dynamic info mismatch" + "\n" + "ref dynamic info : " + refString + "\n" +
-                    "test dynamic info : " + testString);
+            fail(hdrInfoKey + " mismatch in codec " + mCodecName + "\nref info : " + refString +
+                    "\n test info : " + testString);
         }
     }
 
@@ -1693,15 +1707,8 @@
                 int height = format.getInteger(MediaFormat.KEY_HEIGHT);
                 int stride = format.getInteger(MediaFormat.KEY_STRIDE);
                 mOutputBuff.checksum(buf, info.size, width, height, stride, bytesPerSample);
-
-                if (mTestDynamicMetadata) {
-                    validateHDRDynamicMetaData(mCodec.getOutputFormat(), ByteBuffer
-                            .wrap(loadByteArrayFromString(HDR_DYNAMIC_INFO[mOutputCount])));
-
-                }
             }
         }
-
         if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
             mSawOutputEOS = true;
         }
@@ -1821,57 +1828,18 @@
         mCodec.release();
         mExtractor.release();
     }
-
-    void validateHDRStaticMetaData(String parent, String name, ByteBuffer HDRStatic,
-                                   boolean ignoreContainerStaticInfo)
-            throws IOException, InterruptedException {
-        mOutputBuff = new OutputManager();
-        MediaFormat format = setUpSource(parent, name);
-        if (ignoreContainerStaticInfo) {
-            format.removeKey(MediaFormat.KEY_HDR_STATIC_INFO);
-        }
-        mCodec = MediaCodec.createByCodecName(mCodecName);
-        configureCodec(format, true, true, false);
-        mCodec.start();
-        doWork(10);
-        queueEOS();
-        waitForAllOutputs();
-        validateHDRStaticMetaData(mCodec.getOutputFormat(), HDRStatic);
-        mCodec.stop();
-        mCodec.release();
-        mExtractor.release();
-    }
-
-    void validateHDRDynamicMetaData(String parent, String name, boolean ignoreContainerDynamicInfo)
-            throws IOException, InterruptedException {
-        mOutputBuff = new OutputManager();
-        MediaFormat format = setUpSource(parent, name);
-        if (ignoreContainerDynamicInfo) {
-            format.removeKey(MediaFormat.KEY_HDR10_PLUS_INFO);
-        }
-        mCodec = MediaCodec.createByCodecName(mCodecName);
-        configureCodec(format, true, true, false);
-        mCodec.start();
-        doWork(10);
-        queueEOS();
-        waitForAllOutputs();
-        mCodec.stop();
-        mCodec.release();
-        mExtractor.release();
-    }
 }
 
 class CodecEncoderTestBase extends CodecTestBase {
     private static final String LOG_TAG = CodecEncoderTestBase.class.getSimpleName();
 
     // files are in WorkDir.getMediaDirString();
-    private static final String INPUT_AUDIO_FILE = "bbb_2ch_44kHz_s16le.raw";
-    private static final String INPUT_VIDEO_FILE = "bbb_cif_yuv420p_30fps.yuv";
+    protected static final String INPUT_AUDIO_FILE = "bbb_2ch_44kHz_s16le.raw";
+    protected static final String INPUT_VIDEO_FILE = "bbb_cif_yuv420p_30fps.yuv";
     protected static final String INPUT_AUDIO_FILE_HBD = "audio/sd_2ch_48kHz_f32le.raw";
     protected static final String INPUT_VIDEO_FILE_HBD = "cosmat_cif_24fps_yuv420p16le.yuv";
-
-    private final int INP_FRM_WIDTH = 352;
-    private final int INP_FRM_HEIGHT = 288;
+    protected final int INP_FRM_WIDTH = 352;
+    protected final int INP_FRM_HEIGHT = 288;
 
     final String mMime;
     final int[] mBitrates;
@@ -2230,3 +2198,241 @@
         return cdtb.mOutputBuff.getBuffer();
     }
 }
+
+class HDRDecoderTestBase extends CodecDecoderTestBase {
+    private static final String LOG_TAG = HDRDecoderTestBase.class.getSimpleName();
+
+    private ByteBuffer mHdrStaticInfoRef;
+    private ByteBuffer mHdrStaticInfoStream;
+    private ByteBuffer mHdrStaticInfoContainer;
+    private Map<Integer, String> mHdrDynamicInfoRef;
+    private Map<Integer, String> mHdrDynamicInfoStream;
+    private Map<Integer, String> mHdrDynamicInfoContainer;
+    private String mHdrDynamicInfoCurrent;
+
+    public HDRDecoderTestBase(String decoder, String mime, String testFile) {
+        super(decoder, mime, testFile);
+    }
+
+    void enqueueInput(int bufferIndex) {
+        if (mHdrDynamicInfoContainer != null && mHdrDynamicInfoContainer.containsKey(mInputCount) &&
+                mExtractor.getSampleSize() != -1) {
+            insertHdrDynamicInfo(
+                    loadByteArrayFromString(mHdrDynamicInfoContainer.get(mInputCount)));
+        }
+        super.enqueueInput(bufferIndex);
+    }
+
+    void dequeueOutput(int bufferIndex, MediaCodec.BufferInfo info) {
+        if (info.size > 0 && mHdrDynamicInfoRef != null) {
+            MediaFormat format = mCodec.getOutputFormat(bufferIndex);
+            if (mHdrDynamicInfoRef.containsKey(mOutputCount)) {
+                mHdrDynamicInfoCurrent = mHdrDynamicInfoRef.get(mOutputCount);
+            }
+            validateHDRInfo(format, MediaFormat.KEY_HDR10_PLUS_INFO,
+                    ByteBuffer.wrap(loadByteArrayFromString(mHdrDynamicInfoCurrent)));
+        }
+        super.dequeueOutput(bufferIndex, info);
+    }
+
+    void validateHDRInfo(String hdrStaticInfoStream, String hdrStaticInfoContainer,
+                         Map<Integer, String> hdrDynamicInfoStream,
+                         Map<Integer, String> hdrDynamicInfoContainer) throws IOException,
+            InterruptedException {
+        mHdrStaticInfoStream = hdrStaticInfoStream != null ?
+                ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfoStream)) : null;
+        mHdrStaticInfoContainer = hdrStaticInfoContainer != null ?
+                ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfoContainer)) : null;
+        mHdrStaticInfoRef = mHdrStaticInfoStream == null ? mHdrStaticInfoContainer :
+                mHdrStaticInfoStream;
+        mHdrDynamicInfoStream = hdrDynamicInfoStream;
+        mHdrDynamicInfoContainer = hdrDynamicInfoContainer;
+        mHdrDynamicInfoRef = hdrDynamicInfoStream == null ? hdrDynamicInfoContainer :
+                hdrDynamicInfoStream;
+
+        assertTrue("reference hdr10/hdr10+ info is not supplied for validation",
+                mHdrDynamicInfoRef != null || mHdrStaticInfoRef != null);
+
+        if (mHdrDynamicInfoStream != null || mHdrDynamicInfoContainer != null) {
+            Assume.assumeNotNull("Test is only applicable to codecs that have HDR10+ profiles",
+                    mProfileHdr10PlusMap.get(mMime));
+        }
+        if (mHdrStaticInfoStream != null || mHdrStaticInfoContainer != null) {
+            Assume.assumeNotNull("Test is only applicable to codecs that have HDR10 profiles",
+                    mProfileHdr10Map.get(mMime));
+        }
+
+        File fObj = new File(mTestFile);
+        String parent = fObj.getParent();
+        if (parent != null) parent += File.separator;
+        else parent = mInpPrefix;
+        Preconditions.assertTestFileExists(parent + fObj.getName());
+        // For decoders, if you intend to supply hdr10+ info using external means like json, make
+        // sure that info that is being supplied is in sync with SEI info
+        if (mHdrDynamicInfoStream != null && mHdrDynamicInfoContainer != null) {
+            assertEquals("Container hdr10+ info size and elementary stream SEI hdr10+ " +
+                            "info size are unequal", mHdrDynamicInfoStream.size(),
+                    mHdrDynamicInfoContainer.size());
+            for (Map.Entry<Integer, String> element : mHdrDynamicInfoStream.entrySet()) {
+                assertTrue("Container hdr10+ info and elementary stream SEI hdr10+ " +
+                                "info frame positions are not in sync",
+                        mHdrDynamicInfoContainer.containsKey(element.getKey()));
+            }
+        }
+        mOutputBuff = new OutputManager();
+        MediaFormat format = setUpSource(parent, fObj.getName());
+        if (mHdrDynamicInfoStream != null || mHdrDynamicInfoContainer != null) {
+            format.setInteger(MediaFormat.KEY_PROFILE, mProfileHdr10PlusMap.get(mMime)[0]);
+        } else {
+            format.setInteger(MediaFormat.KEY_PROFILE, mProfileHdr10Map.get(mMime)[0]);
+        }
+        ArrayList<MediaFormat> formatList = new ArrayList<>();
+        formatList.add(format);
+        Assume.assumeTrue(mCodecName + " does not support HDR10/HDR10+ profile",
+                areFormatsSupported(mCodecName, mMime, formatList));
+        mCodec = MediaCodec.createByCodecName(mCodecName);
+        configureCodec(format, false, true, false);
+        mCodec.start();
+        doWork(Integer.MAX_VALUE);
+        queueEOS();
+        waitForAllOutputs();
+        if (mHdrStaticInfoRef != null) {
+            validateHDRInfo(mCodec.getOutputFormat(), MediaFormat.KEY_HDR_STATIC_INFO,
+                    mHdrStaticInfoRef);
+        }
+        mCodec.stop();
+        mCodec.release();
+        mExtractor.release();
+    }
+}
+
+class HDREncoderTestBase extends CodecEncoderTestBase {
+    private static final String LOG_TAG = HDREncoderTestBase.class.getSimpleName();
+
+    private ByteBuffer mHdrStaticInfo;
+    private Map<Integer, String> mHdrDynamicInfo;
+
+    private MediaMuxer mMuxer;
+    private int mTrackID = -1;
+
+    public HDREncoderTestBase(String encoderName, String mediaType, int bitrate, int width,
+                              int height) {
+        super(encoderName, mediaType, new int[]{bitrate}, new int[]{width}, new int[]{height});
+    }
+
+    void enqueueInput(int bufferIndex) {
+        if (mHdrDynamicInfo != null && mHdrDynamicInfo.containsKey(mInputCount)) {
+            insertHdrDynamicInfo(loadByteArrayFromString(mHdrDynamicInfo.get(mInputCount)));
+        }
+        super.enqueueInput(bufferIndex);
+    }
+
+    void dequeueOutput(int bufferIndex, MediaCodec.BufferInfo info) {
+        MediaFormat bufferFormat = mCodec.getOutputFormat(bufferIndex);
+        if (info.size > 0) {
+            ByteBuffer buf = mCodec.getOutputBuffer(bufferIndex);
+            if (mMuxer != null) {
+                if (mTrackID == -1) {
+                    mTrackID = mMuxer.addTrack(bufferFormat);
+                    mMuxer.start();
+                }
+                mMuxer.writeSampleData(mTrackID, buf, info);
+            }
+        }
+        super.dequeueOutput(bufferIndex, info);
+    }
+
+    void validateHDRInfo(String hdrStaticInfo, Map<Integer, String> hdrDynamicInfo)
+            throws IOException, InterruptedException {
+        mHdrStaticInfo = hdrStaticInfo != null ?
+                ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfo)) : null;
+        mHdrDynamicInfo = hdrDynamicInfo;
+
+        setUpParams(1);
+
+        MediaFormat format = mFormats.get(0);
+        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, COLOR_FormatYUVP010);
+        format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED);
+        format.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT2020);
+        format.setInteger(MediaFormat.KEY_COLOR_TRANSFER, MediaFormat.COLOR_TRANSFER_ST2084);
+        int profile = (mHdrDynamicInfo != null) ? mProfileHdr10PlusMap.get(mMime)[0] :
+                mProfileHdr10Map.get(mMime)[0];
+        format.setInteger(MediaFormat.KEY_PROFILE, profile);
+
+        if (mHdrStaticInfo != null) {
+            format.setByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, mHdrStaticInfo);
+        }
+        Assume.assumeTrue(mCodecName + " does not support HDR10/HDR10+ profile " + profile,
+                areFormatsSupported(mCodecName, mMime, mFormats));
+        Assume.assumeTrue(mCodecName + " does not support color format COLOR_FormatYUVP010",
+                hasSupportForColorFormat(mCodecName, mMime, COLOR_FormatYUVP010));
+
+        mBytesPerSample = 2;
+        setUpSource(INPUT_VIDEO_FILE_HBD);
+
+        int frameLimit = 4;
+        if (mHdrDynamicInfo != null) {
+            Integer lastHdr10PlusFrame =
+                    Collections.max(HDR_DYNAMIC_INFO.entrySet(), Map.Entry.comparingByKey())
+                            .getKey();
+            frameLimit = lastHdr10PlusFrame + 10;
+        }
+        int maxNumFrames = mInputData.length / (INP_FRM_WIDTH * INP_FRM_HEIGHT * mBytesPerSample);
+        assertTrue("HDR info tests require input file with at least " + frameLimit +
+                        " frames. " + INPUT_VIDEO_FILE_HBD + " has " + maxNumFrames + " frames.",
+                frameLimit <= maxNumFrames);
+
+        mOutputBuff = new OutputManager();
+        mCodec = MediaCodec.createByCodecName(mCodecName);
+        File tmpFile;
+        int muxerFormat;
+        if (mMime.equals(MediaFormat.MIMETYPE_VIDEO_VP9)) {
+            muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM;
+            tmpFile = File.createTempFile("tmp10bit", ".webm");
+        } else {
+            muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
+            tmpFile = File.createTempFile("tmp10bit", ".mp4");
+        }
+        mMuxer = new MediaMuxer(tmpFile.getAbsolutePath(), muxerFormat);
+        configureCodec(format, true, true, true);
+        mCodec.start();
+        doWork(frameLimit);
+        queueEOS();
+        waitForAllOutputs();
+        if (mTrackID != -1) {
+            mMuxer.stop();
+            mTrackID = -1;
+        }
+        if (mMuxer != null) {
+            mMuxer.release();
+            mMuxer = null;
+        }
+        String log = String.format("format: %s \n codec: %s:: ", format, mCodecName);
+        assertTrue(log + "unexpected error", !mAsyncHandle.hasSeenError());
+        assertTrue(log + "no input sent", 0 != mInputCount);
+        assertTrue(log + "output received", 0 != mOutputCount);
+
+        MediaFormat fmt = mCodec.getOutputFormat();
+
+        mCodec.stop();
+        mCodec.release();
+        if (mHdrStaticInfo != null) {
+            // verify if the out fmt contains HDR Static info as expected
+            validateHDRInfo(fmt, MediaFormat.KEY_HDR_STATIC_INFO, mHdrStaticInfo);
+        }
+
+        // verify if the muxed file contains HDR Dynamic info as expected
+        MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+        String decoder = codecList.findDecoderForFormat(format);
+        assertNotNull("Device advertises support for encoding " + format + " but not decoding it",
+                decoder);
+
+        HDRDecoderTestBase decoderTest = new HDRDecoderTestBase(decoder, mMime,
+                tmpFile.getAbsolutePath());
+        decoderTest.validateHDRInfo(hdrStaticInfo, hdrStaticInfo, mHdrDynamicInfo, mHdrDynamicInfo);
+        if (HDR_INFO_IN_BITSTREAM_CODECS.contains(mMime)) {
+            decoderTest.validateHDRInfo(hdrStaticInfo, null, mHdrDynamicInfo, null);
+        }
+        tmpFile.delete();
+    }
+}
diff --git a/tests/media/src/android/mediav2/cts/DecoderHDRInfoTest.java b/tests/media/src/android/mediav2/cts/DecoderHDRInfoTest.java
index 3dd28aa..4994a9b 100644
--- a/tests/media/src/android/mediav2/cts/DecoderHDRInfoTest.java
+++ b/tests/media/src/android/mediav2/cts/DecoderHDRInfoTest.java
@@ -22,6 +22,8 @@
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
+import com.android.compatibility.common.util.CddTest;
+
 import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,34 +31,34 @@
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
- * Test to validate hdr static metadata in decoders
+ * Test to validate hdr static info in decoders
  */
 @RunWith(Parameterized.class)
 // P010 support was added in Android T, hence limit the following tests to Android T and above
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
-public class DecoderHDRInfoTest extends CodecDecoderTestBase {
+public class DecoderHDRInfoTest extends HDRDecoderTestBase {
     private static final String LOG_TAG = DecoderHDRInfoTest.class.getSimpleName();
-    private static final String HDR_STATIC_INFO =
-            "00 d0 84 80 3e c2 33 c4 86 4c 1d b8 0b 13 3d 42 40 a0 0f 32 00 10 27 df 0d";
-    private static final String HDR_STATIC_INCORRECT_INFO =
-            "00 d0 84 80 3e c2 33 c4 86 10 27 d0 07 13 3d 42 40 a0 0f 32 00 10 27 df 0d";
 
-    private final ByteBuffer mHDRStaticInfoStream;
-    private final ByteBuffer mHDRStaticInfoContainer;
+    private String mHDRStaticInfoStream;
+    private String mHDRStaticInfoContainer;
+    private Map<Integer, String> mHDRDynamicInfoStream;
+    private Map<Integer, String> mHDRDynamicInfoContainer;
 
     public DecoderHDRInfoTest(String codecName, String mediaType, String testFile,
-                              String hdrStaticInfoStream, String hdrStaticInfoContainer) {
+                              String hdrStaticInfoStream, String hdrStaticInfoContainer,
+                              Map<Integer, String> HDRDynamicInfoStream,
+                              Map<Integer, String> HDRDynamicInfoContainer) {
         super(codecName, mediaType, testFile);
-        mHDRStaticInfoStream = hdrStaticInfoStream != null ?
-                ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfoStream)) : null;
-        mHDRStaticInfoContainer = hdrStaticInfoContainer != null ?
-                ByteBuffer.wrap(loadByteArrayFromString(hdrStaticInfoContainer)) : null;
+        mHDRStaticInfoStream = hdrStaticInfoStream;
+        mHDRStaticInfoContainer = hdrStaticInfoContainer;
+        mHDRDynamicInfoStream = HDRDynamicInfoStream;
+        mHDRDynamicInfoContainer = HDRDynamicInfoContainer;
     }
 
     @Parameterized.Parameters(name = "{index}({0}_{1})")
@@ -65,64 +67,50 @@
         final boolean needAudio = false;
         final boolean needVideo = true;
         final List<Object[]> exhaustiveArgsList = Arrays.asList(new Object[][]{
-                // codecMediaType, testFile, hdrInfo in stream, hdrInfo in container
+                // codecMediaType, testFile, hdrStaticInfo in stream, hdrStaticInfo in container,
+                // hdrDynamicInfo in stream, hdrDynamicInfo in container
                 {MediaFormat.MIMETYPE_VIDEO_HEVC,
                         "cosmat_352x288_hdr10_stream_and_container_correct_hevc.mkv",
-                        HDR_STATIC_INFO, HDR_STATIC_INFO},
+                        HDR_STATIC_INFO, HDR_STATIC_INFO, null, null},
                 {MediaFormat.MIMETYPE_VIDEO_HEVC,
                         "cosmat_352x288_hdr10_stream_correct_container_incorrect_hevc.mkv",
-                        HDR_STATIC_INFO, HDR_STATIC_INCORRECT_INFO},
+                        HDR_STATIC_INFO, HDR_STATIC_INCORRECT_INFO, null, null},
                 {MediaFormat.MIMETYPE_VIDEO_HEVC, "cosmat_352x288_hdr10_only_stream_hevc.mkv",
-                        HDR_STATIC_INFO, null},
+                        HDR_STATIC_INFO, null, null, null},
                 {MediaFormat.MIMETYPE_VIDEO_HEVC, "cosmat_352x288_hdr10_only_container_hevc.mkv",
-                        null, HDR_STATIC_INFO},
+                        null, HDR_STATIC_INFO, null, null},
                 {MediaFormat.MIMETYPE_VIDEO_VP9, "cosmat_352x288_hdr10_only_container_vp9.mkv",
-                        null, HDR_STATIC_INFO},
+                        null, HDR_STATIC_INFO, null, null},
                 {MediaFormat.MIMETYPE_VIDEO_AV1,
                         "cosmat_352x288_hdr10_stream_and_container_correct_av1.mkv",
-                        HDR_STATIC_INFO, HDR_STATIC_INFO},
+                        HDR_STATIC_INFO, HDR_STATIC_INFO, null, null},
                 {MediaFormat.MIMETYPE_VIDEO_AV1,
                         "cosmat_352x288_hdr10_stream_correct_container_incorrect_av1.mkv",
-                        HDR_STATIC_INFO, HDR_STATIC_INCORRECT_INFO},
+                        HDR_STATIC_INFO, HDR_STATIC_INCORRECT_INFO, null, null},
                 {MediaFormat.MIMETYPE_VIDEO_AV1, "cosmat_352x288_hdr10_only_stream_av1.mkv",
-                        HDR_STATIC_INFO, null},
+                        HDR_STATIC_INFO, null, null, null},
                 {MediaFormat.MIMETYPE_VIDEO_AV1, "cosmat_352x288_hdr10_only_container_av1.mkv",
-                        null, HDR_STATIC_INFO},
+                        null, HDR_STATIC_INFO, null, null},
+                {MediaFormat.MIMETYPE_VIDEO_HEVC, "cosmat_352x288_hdr10plus_hevc.mp4",
+                        null, null, HDR_DYNAMIC_INFO, null},
+                {MediaFormat.MIMETYPE_VIDEO_HEVC, "cosmat_352x288_hdr10plus_hevc.mp4",
+                        null, null, HDR_DYNAMIC_INFO, HDR_DYNAMIC_INCORRECT_INFO},
+                {MediaFormat.MIMETYPE_VIDEO_AV1, "cosmat_352x288_hdr10plus_av1.mkv",
+                        null, null, HDR_DYNAMIC_INFO, null},
+                {MediaFormat.MIMETYPE_VIDEO_AV1, "cosmat_352x288_hdr10plus_av1.mkv",
+                        null, null, HDR_DYNAMIC_INFO, HDR_DYNAMIC_INCORRECT_INFO},
+                {MediaFormat.MIMETYPE_VIDEO_VP9, "cosmat_352x288_hdr10_only_container_vp9.mkv",
+                        null, null, null, HDR_DYNAMIC_INFO},
+
         });
         return prepareParamList(exhaustiveArgsList, isEncoder, needAudio, needVideo, false);
     }
 
     @SmallTest
     @Test(timeout = PER_TEST_TIMEOUT_SMALL_TEST_MS)
-    public void testHDRMetadata() throws IOException, InterruptedException {
-        int[] Hdr10Profiles = mProfileHdr10Map.get(mMime);
-        Assume.assumeNotNull("Test is only applicable to codecs that have HDR10 profiles",
-                Hdr10Profiles);
-        MediaFormat format = setUpSource(mTestFile);
-        mExtractor.release();
-        ArrayList<MediaFormat> formats = new ArrayList<>();
-        formats.add(format);
-
-        // When HDR metadata isn't present in the container, but included in the bitstream,
-        // extractors may not be able to populate HDR10/HDR10+ profiles correctly.
-        // In such cases, override the profile
-        if (mHDRStaticInfoContainer == null && mHDRStaticInfoStream != null) {
-            int profile = Hdr10Profiles[0];
-            format.setInteger(MediaFormat.KEY_PROFILE, profile);
-        }
-        Assume.assumeTrue(areFormatsSupported(mCodecName, mMime, formats));
-
-        if (mHDRStaticInfoContainer != null) {
-            validateHDRStaticMetaData(format, mHDRStaticInfoContainer);
-        }
-
-        validateHDRStaticMetaData(mInpPrefix, mTestFile,
-                mHDRStaticInfoStream == null ? mHDRStaticInfoContainer : mHDRStaticInfoStream,
-                false);
-        if (mHDRStaticInfoStream != null) {
-            if (EncoderHDRInfoTest.mCheckESList.contains(mMime)) {
-                validateHDRStaticMetaData(mInpPrefix, mTestFile, mHDRStaticInfoStream, true);
-            }
-        }
+    @CddTest(requirements = {"5.3.5/C-3-1", "5.3.7/C-4-1", "5.3.9"})
+    public void testHDRInfo() throws IOException, InterruptedException {
+        validateHDRInfo(mHDRStaticInfoStream, mHDRStaticInfoContainer, mHDRDynamicInfoStream,
+                mHDRDynamicInfoContainer);
     }
 }
diff --git a/tests/media/src/android/mediav2/cts/EncodeDecodeAccuracyTest.java b/tests/media/src/android/mediav2/cts/EncodeDecodeAccuracyTest.java
index ebed139..6bcd5c7 100644
--- a/tests/media/src/android/mediav2/cts/EncodeDecodeAccuracyTest.java
+++ b/tests/media/src/android/mediav2/cts/EncodeDecodeAccuracyTest.java
@@ -27,6 +27,8 @@
 
 import androidx.test.filters.LargeTest;
 
+import com.android.compatibility.common.util.MediaUtils;
+
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
@@ -138,6 +140,11 @@
 
     @Before
     public void setUp() throws IOException {
+        // Few cuttlefish specific color conversion issues were fixed after Android T.
+        if (MediaUtils.onCuttlefish()) {
+            assumeTrue("Color conversion related tests are not valid on cuttlefish releases "
+                    + "through android T", IS_AT_LEAST_U);
+        }
         if (mUseHighBitDepth) {
             assumeTrue("Codec doesn't support ABGR2101010",
                     hasSupportForColorFormat(mCompName, mMime, COLOR_Format32bitABGR2101010));
diff --git a/tests/media/src/android/mediav2/cts/EncoderColorAspectsTest.java b/tests/media/src/android/mediav2/cts/EncoderColorAspectsTest.java
index 1e44a00..a2563a3 100644
--- a/tests/media/src/android/mediav2/cts/EncoderColorAspectsTest.java
+++ b/tests/media/src/android/mediav2/cts/EncoderColorAspectsTest.java
@@ -29,6 +29,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.compatibility.common.util.ApiLevelUtil;
+import com.android.compatibility.common.util.MediaUtils;
 
 import org.junit.Assume;
 import org.junit.Test;
@@ -266,6 +267,11 @@
         if (mSurfaceMode) {
             Assume.assumeTrue("Surface mode tests are limited to devices launching with Android T",
                     FIRST_SDK_IS_AT_LEAST_T && VNDK_IS_AT_LEAST_T);
+            // Few cuttlefish specific color conversion issues were fixed after Android T.
+            if (MediaUtils.onCuttlefish()) {
+                Assume.assumeTrue("Color conversion related tests are not valid on cuttlefish "
+                        + "releases through android T", IS_AT_LEAST_U);
+            }
         }
 
         if (mUseHighBitDepth) {
diff --git a/tests/media/src/android/mediav2/cts/EncoderHDRInfoTest.java b/tests/media/src/android/mediav2/cts/EncoderHDRInfoTest.java
index 26c6eb5..66916e5 100644
--- a/tests/media/src/android/mediav2/cts/EncoderHDRInfoTest.java
+++ b/tests/media/src/android/mediav2/cts/EncoderHDRInfoTest.java
@@ -21,84 +21,59 @@
 import android.media.MediaFormat;
 import android.media.MediaMuxer;
 import android.os.Build;
-import android.os.Bundle;
+import android.util.Log;
 
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
 
+import com.android.compatibility.common.util.CddTest;
+
+import org.junit.After;
 import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
-import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUVP010;
-import static android.media.MediaCodecInfo.CodecProfileLevel.*;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 /**
- * Test to validate hdr static and dynamic metadata in encoders
+ * HDR10 Metadata is an aid for a display device to show the content in an optimal manner. It
+ * contains the HDR content and mastering device properties that are used by the display device
+ * to map the content according to its own color gamut and peak brightness. This information is
+ * part of the elementary stream. Generally this information is placed at scene change intervals
+ * or even at every frame level. If the encoder is configured with hdr info, then it is
+ * expected to place this information in the elementary stream as-is. This test validates the
+ * same. The test feeds per-frame or per-scene info at various points and expects the encoder
+ * to place the hdr info in the elementary stream at exactly those points
+ *
+ * Restrict hdr info test for Android T and above
  */
 @RunWith(Parameterized.class)
 // P010 support was added in Android T, hence limit the following tests to Android T and above
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
-public class EncoderHDRInfoTest extends CodecEncoderTestBase {
+public class EncoderHDRInfoTest extends HDREncoderTestBase {
     private static final String LOG_TAG = EncoderHDRInfoTest.class.getSimpleName();
-    private MediaMuxer mMuxer;
-    private int mTrackID = -1;
 
-    static final ArrayList<String> mCheckESList = new ArrayList<>();
+    private String mHDRStaticInfo;
+    private Map<Integer, String> mHDRDynamicInfo;
 
-    static {
-        mCheckESList.add(MediaFormat.MIMETYPE_VIDEO_AV1);
-        mCheckESList.add(MediaFormat.MIMETYPE_VIDEO_AVC);
-        mCheckESList.add(MediaFormat.MIMETYPE_VIDEO_HEVC);
-    }
-
-    public EncoderHDRInfoTest(String encoderName, String mediaType, int bitrate, int width,
-            int height, boolean testDynamicMetadata) {
-        super(encoderName, mediaType, new int[]{bitrate}, new int[]{width}, new int[]{height});
-        mTestDynamicMetadata = testDynamicMetadata;
-    }
-
-    void enqueueInput(int bufferIndex) {
-        if(mTestDynamicMetadata){
-            final Bundle params = new Bundle();
-            byte[] info = loadByteArrayFromString(HDR_DYNAMIC_INFO[mInputCount]);
-            params.putByteArray(MediaFormat.KEY_HDR10_PLUS_INFO, info);
-            mCodec.setParameters(params);
-            if (mInputCount >= HDR_DYNAMIC_INFO.length) {
-                mSawInputEOS = true;
-            }
-        }
-        super.enqueueInput(bufferIndex);
-    }
-    void dequeueOutput(int bufferIndex, MediaCodec.BufferInfo info) {
-        MediaFormat bufferFormat = mCodec.getOutputFormat(bufferIndex);
-        if (info.size > 0) {
-            ByteBuffer buf = mCodec.getOutputBuffer(bufferIndex);
-            if (mMuxer != null) {
-                if (mTrackID == -1) {
-                    mTrackID = mMuxer.addTrack(bufferFormat);
-                    mMuxer.start();
-                }
-                mMuxer.writeSampleData(mTrackID, buf, info);
-            }
-        }
-        super.dequeueOutput(bufferIndex, info);
-        // verify if the out fmt contains HDR Dynamic metadata as expected
-        if (mTestDynamicMetadata && mOutputCount > 0) {
-            validateHDRDynamicMetaData(bufferFormat,
-                    ByteBuffer.wrap(loadByteArrayFromString(HDR_DYNAMIC_INFO[mOutputCount - 1])));
-        }
+    public EncoderHDRInfoTest(String encoderName, String mediaType, int bitrate,
+                              int width, int height, String HDRStaticInfo,
+                              Map<Integer, String> HDRDynamicInfo) {
+        super(encoderName, mediaType, bitrate, width, height);
+        mHDRStaticInfo = HDRStaticInfo;
+        mHDRDynamicInfo = HDRDynamicInfo;
     }
 
     @Parameterized.Parameters(name = "{index}({0}_{1})")
@@ -106,111 +81,28 @@
         final boolean isEncoder = true;
         final boolean needAudio = false;
         final boolean needVideo = true;
+        final String[] mediaTypes = new String[]{
+                MediaFormat.MIMETYPE_VIDEO_AV1,
+                MediaFormat.MIMETYPE_VIDEO_HEVC,
+                MediaFormat.MIMETYPE_VIDEO_VP9
+        };
 
-        final List<Object[]> exhaustiveArgsList = Arrays.asList(new Object[][]{
-                {MediaFormat.MIMETYPE_VIDEO_AV1, 512000, 352, 288, false},
-                {MediaFormat.MIMETYPE_VIDEO_VP9, 512000, 352, 288, false},
-                {MediaFormat.MIMETYPE_VIDEO_HEVC, 512000, 352, 288, false},
-
-                {MediaFormat.MIMETYPE_VIDEO_AV1, 512000, 352, 288, true},
-                {MediaFormat.MIMETYPE_VIDEO_VP9, 512000, 352, 288, true},
-                {MediaFormat.MIMETYPE_VIDEO_HEVC, 512000, 352, 288, true},
-        });
+        final List<Object[]> exhaustiveArgsList = new ArrayList<>();
+        for (String mediaType : mediaTypes) {
+            // mediaType, bitrate, width, height, hdrStaticInfo, hdrDynamicInfo
+            exhaustiveArgsList.add(new Object[]{mediaType, 512000, 352, 288, HDR_STATIC_INFO,
+                    null});
+            exhaustiveArgsList.add(new Object[]{mediaType, 512000, 352, 288, null,
+                    HDR_DYNAMIC_INFO});
+        }
 
         return prepareParamList(exhaustiveArgsList, isEncoder, needAudio, needVideo, false);
     }
 
     @SmallTest
     @Test(timeout = PER_TEST_TIMEOUT_SMALL_TEST_MS)
-    public void testHDRMetadata() throws IOException, InterruptedException {
-        int profile;
-        setUpParams(1);
-        MediaFormat format = mFormats.get(0);
-        final ByteBuffer hdrStaticInfo = ByteBuffer.wrap(loadByteArrayFromString(HDR_STATIC_INFO));
-        if (mTestDynamicMetadata) {
-            profile = mProfileHdr10PlusMap.getOrDefault(mMime, new int[]{-1})[0];
-        } else {
-            profile = mProfileHdr10Map.getOrDefault(mMime, new int[]{-1})[0];
-        }
-        format.setInteger(MediaFormat.KEY_PROFILE, profile);
-        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, COLOR_FormatYUVP010);
-        format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED);
-        format.setInteger(MediaFormat.KEY_COLOR_STANDARD, MediaFormat.COLOR_STANDARD_BT2020);
-        format.setInteger(MediaFormat.KEY_COLOR_TRANSFER, MediaFormat.COLOR_TRANSFER_ST2084);
-        format.setByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO, hdrStaticInfo);
-        mFormats.clear();
-        mFormats.add(format);
-        Assume.assumeTrue(mCodecName + " does not support this HDR profile",
-                areFormatsSupported(mCodecName, mMime, mFormats));
-        Assume.assumeTrue(mCodecName + " does not support color format COLOR_FormatYUVP010",
-                hasSupportForColorFormat(mCodecName, mMime, COLOR_FormatYUVP010));
-        mBytesPerSample = 2;
-        setUpSource(INPUT_VIDEO_FILE_HBD);
-        mOutputBuff = new OutputManager();
-        mCodec = MediaCodec.createByCodecName(mCodecName);
-        mOutputBuff.reset();
-        String log = String.format("format: %s \n codec: %s:: ", format, mCodecName);
-        File tmpFile;
-        int muxerFormat;
-        if (mMime.equals(MediaFormat.MIMETYPE_VIDEO_VP9)) {
-            muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM;
-            tmpFile = File.createTempFile("tmp10bit", ".webm");
-        } else {
-            muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
-            tmpFile = File.createTempFile("tmp10bit", ".mp4");
-        }
-        mMuxer = new MediaMuxer(tmpFile.getAbsolutePath(), muxerFormat);
-        configureCodec(format, true, true, true);
-        mCodec.start();
-        doWork(4);
-        queueEOS();
-        waitForAllOutputs();
-        if (mTrackID != -1) {
-            mMuxer.stop();
-            mTrackID = -1;
-        }
-        if (mMuxer != null) {
-            mMuxer.release();
-            mMuxer = null;
-        }
-        assertTrue(log + "unexpected error", !mAsyncHandle.hasSeenError());
-        assertTrue(log + "no input sent", 0 != mInputCount);
-        assertTrue(log + "output received", 0 != mOutputCount);
-
-        MediaFormat fmt = mCodec.getOutputFormat();
-        mCodec.stop();
-        mCodec.release();
-
-        // verify if the out fmt contains HDR Static metadata as expected
-        validateHDRStaticMetaData(fmt, hdrStaticInfo);
-
-        // verify if the muxed file contains HDR metadata as expected
-        MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
-        String decoder = codecList.findDecoderForFormat(format);
-        assertNotNull("Device advertises support for encoding " + format.toString() +
-                " but not decoding it", decoder);
-        CodecDecoderTestBase cdtb =
-                new CodecDecoderTestBase(decoder, mMime, tmpFile.getAbsolutePath());
-        String parent = tmpFile.getParent();
-        if (parent != null) parent += File.separator;
-        else parent = "";
-        cdtb.validateHDRStaticMetaData(parent, tmpFile.getName(), hdrStaticInfo, false);
-        if (mTestDynamicMetadata) {
-            cdtb.validateHDRDynamicMetaData(parent, tmpFile.getName(), false);
-        }
-
-        // if HDR static metadata can also be signalled via elementary stream then verify if
-        // the elementary stream contains HDR static data as expected
-        if (mCheckESList.contains(mMime)) {
-            cdtb.validateHDRStaticMetaData(parent, tmpFile.getName(), hdrStaticInfo, true);
-
-            // since HDR static metadata is signalled via elementary stream then verify if
-            // the elementary stream contains HDR static data as expected
-            if (mTestDynamicMetadata) {
-                cdtb.validateHDRDynamicMetaData(parent, tmpFile.getName(), true);
-            }
-        }
-
-        tmpFile.delete();
+    @CddTest(requirements = {"5.12/C-6-4"})
+    public void testHDRInfo() throws IOException, InterruptedException {
+        validateHDRInfo(mHDRStaticInfo, mHDRDynamicInfo);
     }
 }
diff --git a/tests/media/src/android/mediav2/cts/EncoderProfileLevelTest.java b/tests/media/src/android/mediav2/cts/EncoderProfileLevelTest.java
index cbf1d36..59eb519 100644
--- a/tests/media/src/android/mediav2/cts/EncoderProfileLevelTest.java
+++ b/tests/media/src/android/mediav2/cts/EncoderProfileLevelTest.java
@@ -128,12 +128,11 @@
                 {MediaFormat.MIMETYPE_VIDEO_MPEG4, 3000000, 704, 576, 30},
                 {MediaFormat.MIMETYPE_VIDEO_MPEG4, 8000000, 720, 576, 30},
 
-                // TODO (b/151430764)
-                /*{MediaFormat.MIMETYPE_VIDEO_VP9, 200000, 256, 144, 15},
+                {MediaFormat.MIMETYPE_VIDEO_VP9, 200000, 256, 144, 15},
                 {MediaFormat.MIMETYPE_VIDEO_VP9, 8000000, 384, 192, 30},
                 {MediaFormat.MIMETYPE_VIDEO_VP9, 1800000, 480, 256, 30},
                 {MediaFormat.MIMETYPE_VIDEO_VP9, 3600000, 640, 384, 30},
-                {MediaFormat.MIMETYPE_VIDEO_VP9, 7200000, 1080, 512, 30},*/
+                {MediaFormat.MIMETYPE_VIDEO_VP9, 7200000, 1080, 512, 30},
                 {MediaFormat.MIMETYPE_VIDEO_VP9, 12000000, 1280, 768, 30},
                 {MediaFormat.MIMETYPE_VIDEO_VP9, 18000000, 2048, 1088, 30},
                 {MediaFormat.MIMETYPE_VIDEO_VP9, 30000000, 2048, 1088, 60},
diff --git a/tests/media/src/android/mediav2/cts/ExtractorTest.java b/tests/media/src/android/mediav2/cts/ExtractorTest.java
index e07386f..2ec1181 100644
--- a/tests/media/src/android/mediav2/cts/ExtractorTest.java
+++ b/tests/media/src/android/mediav2/cts/ExtractorTest.java
@@ -684,7 +684,7 @@
         private void checkExtractorOkForUrlDS(Map<String, String> headers) throws Exception {
             MediaExtractor testExtractor = new MediaExtractor();
             testExtractor.setDataSource(mInpMediaUrl, headers);
-            HttpRequest req = mWebServer.getLastRequest(mResString);
+            HttpRequest req = mWebServer.getLastAssetRequest(mResString);
             if (headers != null) {
                 for (String key : headers.keySet()) {
                     String value = headers.get(key);
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/tests/appop/Android.bp b/tests/tests/appop/Android.bp
index beede26..748e344 100644
--- a/tests/tests/appop/Android.bp
+++ b/tests/tests/appop/Android.bp
@@ -74,7 +74,7 @@
         "androidx.legacy_legacy-support-v4",
         "platform-test-annotations",
         "truth-prebuilt",
-        "androidx.test.uiautomator_uiautomator"
+        "androidx.test.uiautomator_uiautomator",
     ],
 
     jni_libs: [
@@ -95,7 +95,6 @@
         "libnativehelper",
         "libnetdutils",
         "libnetworkstats",
-        "libnetworkstatsfactorytestjni",
         "libpackagelistparser",
         "libpermission",
         "libpcre2",
diff --git a/tests/tests/bluetooth/AndroidTest.xml b/tests/tests/bluetooth/AndroidTest.xml
index 9a3075b..caf9428 100644
--- a/tests/tests/bluetooth/AndroidTest.xml
+++ b/tests/tests/bluetooth/AndroidTest.xml
@@ -33,6 +33,7 @@
     <!-- Only run Cts Tests in MTS if the Bluetooth Mainline module is installed. -->
     <object type="module_controller"
             class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
-        <option name="mainline-module-package-name" value="com.google.android.bluetooth" />
+        <option name="mainline-module-package-name" value="com.android.btservices" />
+        <option name="mainline-module-package-name" value="com.google.android.btservices" />
     </object>
 </configuration>
diff --git a/tests/tests/hibernation/Android.bp b/tests/tests/hibernation/Android.bp
new file mode 100644
index 0000000..3bb187f
--- /dev/null
+++ b/tests/tests/hibernation/Android.bp
@@ -0,0 +1,65 @@
+// 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 {
+    name: "CtsHibernationTestCases",
+    defaults: [
+        "cts_defaults",
+        "mts-target-sdk-version-current",
+    ],
+    compile_multilib: "both",
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "androidx.test.uiautomator_uiautomator",
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "testng",
+        "truth-prebuilt",
+        "guava",
+        "junit",
+        "hamcrest-library",
+        "modules-utils-build_system",
+        "safety-center-internal-data",
+    ],
+    srcs: [
+        "src/**/*.kt",
+    ],
+    test_config: "AndroidTest.xml",
+    // Tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "general-tests",
+        "mts-permission",
+    ],
+    sdk_version: "test_current",
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    // Do not compress minijail policy files.
+    aaptflags: ["-0 .policy"],
+    min_sdk_version: "29",
+    data: [
+        ":CtsAutoRevokeQApp",
+        ":CtsAutoRevokeSApp",
+        ":CtsAutoRevokeRApp",
+    ],
+    per_testcase_directory: true,
+}
diff --git a/tests/tests/hibernation/AndroidManifest.xml b/tests/tests/hibernation/AndroidManifest.xml
new file mode 100644
index 0000000..80ccd37
--- /dev/null
+++ b/tests/tests/hibernation/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.hibernation.cts">
+
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="android.hibernation.cts"
+         android:label="CTS tests of android.hibernation">
+        <meta-data android:name="listener"
+             android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+
+</manifest>
diff --git a/tests/tests/hibernation/AndroidTest.xml b/tests/tests/hibernation/AndroidTest.xml
new file mode 100644
index 0000000..ef56d17
--- /dev/null
+++ b/tests/tests/hibernation/AndroidTest.xml
@@ -0,0 +1,46 @@
+<?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.
+-->
+<configuration description="Configuration for Hibernation Tests">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="framework" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.permission.apex" />
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="CtsHibernationTestCases.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.hibernation.cts" />
+        <option name="runtime-hint" value="3m15s" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+
+    <!-- Create Place to store apks -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="mkdir -p /data/local/tmp/cts/hibernation" />
+        <option name="run-command" value="am wait-for-broadcast-idle" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/cts/hibernation" />
+    </target_preparer>
+    <!-- Load additional APKs onto device -->
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="push" value="CtsAutoRevokeSApp.apk->/data/local/tmp/cts/hibernation/CtsAutoRevokeSApp.apk" />
+        <option name="push" value="CtsAutoRevokeRApp.apk->/data/local/tmp/cts/hibernation/CtsAutoRevokeRApp.apk" />
+        <option name="push" value="CtsAutoRevokeQApp.apk->/data/local/tmp/cts/hibernation/CtsAutoRevokeQApp.apk" />
+    </target_preparer>
+</configuration>
diff --git a/tests/tests/os/AutoRevokeQApp/Android.bp b/tests/tests/hibernation/AutoRevokeQApp/Android.bp
similarity index 93%
rename from tests/tests/os/AutoRevokeQApp/Android.bp
rename to tests/tests/hibernation/AutoRevokeQApp/Android.bp
index 5c7d4b9..a4a57fa 100644
--- a/tests/tests/os/AutoRevokeQApp/Android.bp
+++ b/tests/tests/hibernation/AutoRevokeQApp/Android.bp
@@ -33,5 +33,8 @@
         "general-tests",
         "sts",
     ],
-    srcs: ["src/**/*.java", "src/**/*.kt"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
 }
diff --git a/tests/tests/os/AutoRevokeQApp/AndroidManifest.xml b/tests/tests/hibernation/AutoRevokeQApp/AndroidManifest.xml
similarity index 89%
rename from tests/tests/os/AutoRevokeQApp/AndroidManifest.xml
rename to tests/tests/hibernation/AutoRevokeQApp/AndroidManifest.xml
index c6355eb..c307b84 100644
--- a/tests/tests/os/AutoRevokeQApp/AndroidManifest.xml
+++ b/tests/tests/hibernation/AutoRevokeQApp/AndroidManifest.xml
@@ -16,12 +16,12 @@
   -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.os.cts.autorevokeqapp">
+    package="android.hibernation.cts.autorevokeqapp">
 
     <uses-permission android:name="android.permission.READ_CALENDAR" />
 
     <application>
-        <activity android:name="android.os.cts.autorevokeqapp.MainActivity"
+        <activity android:name="android.hibernation.cts.autorevokeqapp.MainActivity"
                   android:exported="true"
                   android:visibleToInstantApps="true" >
             <intent-filter>
diff --git a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt b/tests/tests/hibernation/AutoRevokeQApp/src/android/hibernation/cts/autorevokeqapp/MainActivity.kt
similarity index 94%
rename from tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
rename to tests/tests/hibernation/AutoRevokeQApp/src/android/hibernation/cts/autorevokeqapp/MainActivity.kt
index 101f200..22feb54 100644
--- a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
+++ b/tests/tests/hibernation/AutoRevokeQApp/src/android/hibernation/cts/autorevokeqapp/MainActivity.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.os.cts.autorevokeqapp
+package android.hibernation.cts.autorevokeqapp
 
 import android.app.Activity
 import android.os.Bundle
diff --git a/tests/tests/os/AutoRevokeRApp/Android.bp b/tests/tests/hibernation/AutoRevokeRApp/Android.bp
similarity index 93%
rename from tests/tests/os/AutoRevokeRApp/Android.bp
rename to tests/tests/hibernation/AutoRevokeRApp/Android.bp
index 8e01388..f1d92ca 100644
--- a/tests/tests/os/AutoRevokeRApp/Android.bp
+++ b/tests/tests/hibernation/AutoRevokeRApp/Android.bp
@@ -32,5 +32,8 @@
         "general-tests",
         "sts",
     ],
-    srcs: ["src/**/*.java", "src/**/*.kt"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
 }
diff --git a/tests/tests/os/AutoRevokeRApp/AndroidManifest.xml b/tests/tests/hibernation/AutoRevokeRApp/AndroidManifest.xml
similarity index 90%
rename from tests/tests/os/AutoRevokeRApp/AndroidManifest.xml
rename to tests/tests/hibernation/AutoRevokeRApp/AndroidManifest.xml
index 91777a0..1c2cbb0 100644
--- a/tests/tests/os/AutoRevokeRApp/AndroidManifest.xml
+++ b/tests/tests/hibernation/AutoRevokeRApp/AndroidManifest.xml
@@ -16,14 +16,14 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.os.cts.autorevokerapp">
+    package="android.hibernation.cts.autorevokerapp">
 
     <uses-permission android:name="android.permission.READ_CALENDAR" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
     <application>
-        <activity android:name="android.os.cts.autorevokerapp.MainActivity"
+        <activity android:name="android.hibernation.cts.autorevokerapp.MainActivity"
                   android:exported="true"
                   android:visibleToInstantApps="true" >
             <intent-filter>
diff --git a/tests/tests/os/AutoRevokeRApp/src/android/os/cts/autorevokerapp/MainActivity.kt b/tests/tests/hibernation/AutoRevokeRApp/src/android/hibernation/cts/autorevokerapp/MainActivity.kt
similarity index 97%
rename from tests/tests/os/AutoRevokeRApp/src/android/os/cts/autorevokerapp/MainActivity.kt
rename to tests/tests/hibernation/AutoRevokeRApp/src/android/hibernation/cts/autorevokerapp/MainActivity.kt
index c97f883..5f3d0ae 100644
--- a/tests/tests/os/AutoRevokeRApp/src/android/os/cts/autorevokerapp/MainActivity.kt
+++ b/tests/tests/hibernation/AutoRevokeRApp/src/android/hibernation/cts/autorevokerapp/MainActivity.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.os.cts.autorevokerapp
+package android.hibernation.cts.autorevokerapp
 
 import android.app.Activity
 import android.content.Intent
diff --git a/tests/tests/os/AutoRevokeSApp/Android.bp b/tests/tests/hibernation/AutoRevokeSApp/Android.bp
similarity index 93%
rename from tests/tests/os/AutoRevokeSApp/Android.bp
rename to tests/tests/hibernation/AutoRevokeSApp/Android.bp
index 60ca80f..11255920 100644
--- a/tests/tests/os/AutoRevokeSApp/Android.bp
+++ b/tests/tests/hibernation/AutoRevokeSApp/Android.bp
@@ -32,5 +32,8 @@
         "general-tests",
         "sts",
     ],
-    srcs: ["src/**/*.java", "src/**/*.kt"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
 }
diff --git a/tests/tests/os/AutoRevokeSApp/AndroidManifest.xml b/tests/tests/hibernation/AutoRevokeSApp/AndroidManifest.xml
similarity index 89%
rename from tests/tests/os/AutoRevokeSApp/AndroidManifest.xml
rename to tests/tests/hibernation/AutoRevokeSApp/AndroidManifest.xml
index 08478e8..b33a546 100644
--- a/tests/tests/os/AutoRevokeSApp/AndroidManifest.xml
+++ b/tests/tests/hibernation/AutoRevokeSApp/AndroidManifest.xml
@@ -16,13 +16,13 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.os.cts.autorevokesapp">
+    package="android.hibernation.cts.autorevokesapp">
 
     <uses-permission android:name="android.permission.READ_CALENDAR" />
     <uses-permission android:name="android.permission.BLUETOOTH" />
 
     <application>
-        <activity android:name="android.os.cts.autorevokesapp.MainActivity"
+        <activity android:name="android.hibernation.cts.autorevokesapp.MainActivity"
                   android:exported="true"
                   android:visibleToInstantApps="true" >
             <intent-filter>
diff --git a/tests/tests/os/AutoRevokeSApp/src/android/os/cts/autorevokesapp/MainActivity.kt b/tests/tests/hibernation/AutoRevokeSApp/src/android/hibernation/cts/autorevokesapp/MainActivity.kt
similarity index 97%
rename from tests/tests/os/AutoRevokeSApp/src/android/os/cts/autorevokesapp/MainActivity.kt
rename to tests/tests/hibernation/AutoRevokeSApp/src/android/hibernation/cts/autorevokesapp/MainActivity.kt
index c04efb2..e1f5842 100644
--- a/tests/tests/os/AutoRevokeSApp/src/android/os/cts/autorevokesapp/MainActivity.kt
+++ b/tests/tests/hibernation/AutoRevokeSApp/src/android/hibernation/cts/autorevokesapp/MainActivity.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.os.cts.autorevokesapp
+package android.hibernation.cts.autorevokesapp
 
 import android.app.Activity
 import android.content.Intent
diff --git a/tests/tests/hibernation/OWNERS b/tests/tests/hibernation/OWNERS
new file mode 100644
index 0000000..002ca5d
--- /dev/null
+++ b/tests/tests/hibernation/OWNERS
@@ -0,0 +1,5 @@
+# Bug component: 1051079
+include platform/frameworks/base:/core/java/android/apphibernation/OWNERS
+include platform/frameworks/base:/core/java/android/permission/OWNERS
+
+per-file *AppHibernationIntegration* = file:platform/frameworks/base:/core/java/android/apphibernation/OWNERS
\ No newline at end of file
diff --git a/tests/tests/hibernation/TEST_MAPPING b/tests/tests/hibernation/TEST_MAPPING
new file mode 100644
index 0000000..742357e
--- /dev/null
+++ b/tests/tests/hibernation/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsHibernationTestCases"
+    }
+  ]
+}
diff --git a/tests/tests/os/src/android/os/cts/AppHibernationIntegrationTest.kt b/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationIntegrationTest.kt
similarity index 93%
rename from tests/tests/os/src/android/os/cts/AppHibernationIntegrationTest.kt
rename to tests/tests/hibernation/src/android/hibernation/cts/AppHibernationIntegrationTest.kt
index 3c92c7a..a7c5f49 100644
--- a/tests/tests/os/src/android/os/cts/AppHibernationIntegrationTest.kt
+++ b/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationIntegrationTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.os.cts
+package android.hibernation.cts
 
 import android.app.ActivityManager
 import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE
@@ -36,6 +36,7 @@
 import android.provider.Settings
 import android.support.test.uiautomator.By
 import android.support.test.uiautomator.BySelector
+import android.support.test.uiautomator.UiDevice
 import android.support.test.uiautomator.UiObject2
 import android.support.test.uiautomator.UiScrollable
 import android.support.test.uiautomator.UiSelector
@@ -45,11 +46,13 @@
 import com.android.compatibility.common.util.DisableAnimationRule
 import com.android.compatibility.common.util.FreezeRotationRule
 import com.android.compatibility.common.util.SystemUtil
+import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
 import com.android.compatibility.common.util.SystemUtil.eventually
 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
-import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
 import com.android.compatibility.common.util.UiAutomatorUtils
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import org.hamcrest.CoreMatchers
 import org.hamcrest.Matchers
 import org.junit.After
@@ -64,9 +67,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import org.junit.Ignore
 
 /**
  * Integration test for app hibernation.
@@ -137,7 +137,6 @@
     }
 
     @Test
-    @Ignore("b/201545116")
     fun testUnusedApp_getsForceStopped() {
         withUnusedThresholdMs(TEST_UNUSED_THRESHOLD) {
             withApp(APK_PATH_S_APP, APK_PACKAGE_NAME_S_APP) {
@@ -302,23 +301,25 @@
             context.startActivity(intent)
 
             waitForIdle()
+            UiAutomatorUtils.getUiDevice()
 
             val packageManager = context.packageManager
             val settingsPackage = intent.resolveActivity(packageManager).packageName
             val res = packageManager.getResourcesForApplication(settingsPackage)
             val title = res.getString(
                 res.getIdentifier("unused_apps_switch", "string", settingsPackage))
-                // Settings can have multiple scrollable containers so all of them should be
-                // searched.
-                var toggleFound = UiAutomatorUtils.waitFindObjectOrNull(By.text(title)) != null
-                var i = 0
-                var scrollableObject = UiScrollable(UiSelector().scrollable(true).instance(i))
-                while (!toggleFound && scrollableObject.waitForExists(WAIT_TIME_MS)) {
-                    // The following line should work for both handheld device and car settings.
-                    toggleFound = scrollableObject.scrollTextIntoView(title) ||
-                        UiAutomatorUtils.waitFindObjectOrNull(By.text(title)) != null
-                    scrollableObject = UiScrollable(UiSelector().scrollable(true).instance(++i))
-                }
+
+            // Settings can have multiple scrollable containers so all of them should be
+            // searched.
+            var toggleFound = UiDevice.getInstance(instrumentation)
+                .findObject(UiSelector().text(title))
+                .waitForExists(WAIT_TIME_MS)
+            var i = 0
+            var scrollableObject = UiScrollable(UiSelector().scrollable(true).instance(i))
+            while (!toggleFound && scrollableObject.waitForExists(WAIT_TIME_MS)) {
+                toggleFound = scrollableObject.scrollTextIntoView(title)
+                scrollableObject = UiScrollable(UiSelector().scrollable(true).instance(++i))
+            }
 
             assertTrue("Remove permissions and free up space toggle not found", toggleFound)
         }
diff --git a/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt b/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt
similarity index 91%
rename from tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
rename to tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt
index 3e2aeee..b6f03c8 100644
--- a/tests/tests/os/src/android/os/cts/AppHibernationUtils.kt
+++ b/tests/tests/hibernation/src/android/hibernation/cts/AppHibernationUtils.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.os.cts
+package android.hibernation.cts
 
 import android.app.Activity
 import android.app.ActivityManager
@@ -40,7 +40,6 @@
 import android.util.Log
 import androidx.test.InstrumentationRegistry
 import com.android.compatibility.common.util.ExceptionUtils.wrappingExceptions
-import com.android.compatibility.common.util.FeatureUtil
 import com.android.compatibility.common.util.LogcatInspector
 import com.android.compatibility.common.util.SystemUtil.eventually
 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
@@ -51,17 +50,18 @@
 import com.android.compatibility.common.util.click
 import com.android.compatibility.common.util.depthFirstSearch
 import com.android.compatibility.common.util.textAsString
+import java.io.InputStream
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import org.hamcrest.Matcher
 import org.hamcrest.Matchers
 import org.junit.Assert
 import org.junit.Assert.assertThat
 import org.junit.Assert.assertTrue
-import java.io.InputStream
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 private const val BROADCAST_TIMEOUT_MS = 60000L
 
+const val PROPERTY_SAFETY_CENTER_ENABLED = "safety_center_is_enabled"
 const val HIBERNATION_BOOT_RECEIVER_CLASS_NAME =
     "com.android.permissioncontroller.hibernation.HibernationOnBootReceiver"
 const val ACTION_SET_UP_HIBERNATION =
@@ -69,7 +69,6 @@
 
 const val SYSUI_PKG_NAME = "com.android.systemui"
 const val NOTIF_LIST_ID = "com.android.systemui:id/notification_stack_scroller"
-const val NOTIF_LIST_ID_AUTOMOTIVE = "com.android.systemui:id/notifications"
 const val CLEAR_ALL_BUTTON_ID = "dismiss_text"
 // Time to find a notification. Unlikely, but in cases with a lot of notifications, it may take
 // time to find the notification we're looking for
@@ -79,12 +78,12 @@
 const val CMD_EXPAND_NOTIFICATIONS = "cmd statusbar expand-notifications"
 const val CMD_COLLAPSE = "cmd statusbar collapse"
 
-const val APK_PATH_S_APP = "/data/local/tmp/cts/os/CtsAutoRevokeSApp.apk"
-const val APK_PACKAGE_NAME_S_APP = "android.os.cts.autorevokesapp"
-const val APK_PATH_R_APP = "/data/local/tmp/cts/os/CtsAutoRevokeRApp.apk"
-const val APK_PACKAGE_NAME_R_APP = "android.os.cts.autorevokerapp"
-const val APK_PATH_Q_APP = "/data/local/tmp/cts/os/CtsAutoRevokeQApp.apk"
-const val APK_PACKAGE_NAME_Q_APP = "android.os.cts.autorevokeqapp"
+const val APK_PATH_S_APP = "/data/local/tmp/cts/hibernation/CtsAutoRevokeSApp.apk"
+const val APK_PACKAGE_NAME_S_APP = "android.hibernation.cts.autorevokesapp"
+const val APK_PATH_R_APP = "/data/local/tmp/cts/hibernation/CtsAutoRevokeRApp.apk"
+const val APK_PACKAGE_NAME_R_APP = "android.hibernation.cts.autorevokerapp"
+const val APK_PATH_Q_APP = "/data/local/tmp/cts/hibernation/CtsAutoRevokeQApp.apk"
+const val APK_PACKAGE_NAME_Q_APP = "android.hibernation.cts.autorevokeqapp"
 
 fun runBootCompleteReceiver(context: Context, testTag: String) {
     val pkgManager = context.packageManager
@@ -141,6 +140,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,
@@ -198,6 +205,12 @@
         threshold.toString(), action)
 }
 
+inline fun <T> withSafetyCenterEnabled(action: () -> T): T {
+    return withDeviceConfig(
+        DeviceConfig.NAMESPACE_PRIVACY, PROPERTY_SAFETY_CENTER_ENABLED,
+        true.toString(), action)
+}
+
 fun awaitAppState(pkg: String, stateMatcher: Matcher<Int>) {
     val context: Context = InstrumentationRegistry.getTargetContext()
     eventually {
@@ -220,6 +233,7 @@
 
 fun goHome() {
     runShellCommandOrThrow("input keyevent KEYCODE_HOME")
+    waitForIdle()
 }
 
 /**
@@ -280,12 +294,7 @@
     while (view == null && start + timeoutMs > System.currentTimeMillis()) {
         view = uiDevice.wait(Until.findObject(selector), VIEW_WAIT_TIMEOUT)
         if (view == null) {
-            val notificationListId = if (FeatureUtil.isAutomotive()) {
-                NOTIF_LIST_ID_AUTOMOTIVE
-            } else {
-                NOTIF_LIST_ID
-            }
-            val notificationList = UiScrollable(UiSelector().resourceId(notificationListId))
+            val notificationList = UiScrollable(UiSelector().resourceId(NOTIF_LIST_ID))
             wrappingExceptions({ cause: Throwable? -> UiDumpUtils.wrapWithUiDump(cause) }) {
                 Assert.assertTrue("Notification list view not found",
                     notificationList.waitForExists(VIEW_WAIT_TIMEOUT))
diff --git a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt b/tests/tests/hibernation/src/android/hibernation/cts/AutoRevokeTest.kt
similarity index 75%
rename from tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
rename to tests/tests/hibernation/src/android/hibernation/cts/AutoRevokeTest.kt
index db58721..34955cd 100644
--- a/tests/tests/os/src/android/os/cts/AutoRevokeTest.kt
+++ b/tests/tests/hibernation/src/android/hibernation/cts/AutoRevokeTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.os.cts
+package android.hibernation.cts
 
 import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
 import android.app.Instrumentation
@@ -26,10 +26,13 @@
 import android.content.pm.PackageManager.PERMISSION_DENIED
 import android.content.pm.PackageManager.PERMISSION_GRANTED
 import android.content.res.Resources
-import android.provider.DeviceConfig
 import android.net.Uri
 import android.os.Build
+import android.os.UserHandle
 import android.platform.test.annotations.AppModeFull
+import android.provider.DeviceConfig
+import android.safetycenter.SafetyCenterIssue
+import android.safetycenter.SafetyCenterManager
 import android.support.test.uiautomator.By
 import android.support.test.uiautomator.BySelector
 import android.support.test.uiautomator.UiObject2
@@ -39,6 +42,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
@@ -54,6 +58,13 @@
 import com.android.compatibility.common.util.depthFirstSearch
 import com.android.compatibility.common.util.uiDump
 import com.android.modules.utils.build.SdkLevel
+import com.android.safetycenter.internaldata.SafetyCenterIds
+import com.android.safetycenter.internaldata.SafetyCenterIssueId
+import com.android.safetycenter.internaldata.SafetyCenterIssueKey
+import java.lang.reflect.Modifier
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import java.util.regex.Pattern
 import org.hamcrest.CoreMatchers.containsString
 import org.hamcrest.CoreMatchers.containsStringIgnoringCase
 import org.hamcrest.CoreMatchers.equalTo
@@ -65,16 +76,13 @@
 import org.junit.Assert.assertThat
 import org.junit.Assert.assertTrue
 import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.BeforeClass
 import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.lang.reflect.Modifier
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicReference
-import java.util.regex.Pattern
 
 private const val READ_CALENDAR = "android.permission.READ_CALENDAR"
 private const val BLUETOOTH_CONNECT = "android.permission.BLUETOOTH_CONNECT"
@@ -84,7 +92,6 @@
  */
 @RunWith(AndroidJUnit4::class)
 class AutoRevokeTest {
-
     private val context: Context = InstrumentationRegistry.getTargetContext()
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
 
@@ -96,8 +103,16 @@
     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"
+        private const val UNUSED_APPS_SOURCE_ID = "AndroidPermissionAutoRevoke"
+        private const val UNUSED_APPS_ISSUE_ID = "unused_apps_issue"
 
         @JvmStatic
         @BeforeClass
@@ -180,7 +195,6 @@
 
     @AppModeFull(reason = "Uses separate apps for testing")
     @Test
-    @Ignore("b/201545116")
     fun testUnusedApp_uninstallApp() {
         assumeFalse(
             "Unused apps screen may be unavailable on TV",
@@ -260,6 +274,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() {
         assumeFalse(isHibernationEnabledForPreSApps())
         withUnusedThresholdMs(3L) {
@@ -392,10 +461,99 @@
         }
     }
 
+    @AppModeFull(reason = "Uses separate apps for testing")
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+    @Test
+    fun testAutoRevoke_showsUpInSafetyCenter() {
+        assumeTrue(deviceSupportsSafetyCenter())
+        withSafetyCenterEnabled {
+            withUnusedThresholdMs(3L) {
+                withDummyApp {
+                    startAppAndAcceptPermission()
+
+                    killDummyApp()
+
+                    // Run
+                    runAppHibernationJob(context, LOG_TAG)
+
+                    // Verify
+                    val safetyCenterManager =
+                        context.getSystemService(SafetyCenterManager::class.java)!!
+                    eventually {
+                        val issues = ArrayList<SafetyCenterIssue>()
+                        runWithShellPermissionIdentity {
+                            val safetyCenterData = safetyCenterManager!!.safetyCenterData
+                            issues.addAll(safetyCenterData.issues)
+                        }
+                        val issueId = SafetyCenterIds.encodeToString(
+                                SafetyCenterIssueId.newBuilder()
+                                        .setSafetyCenterIssueKey(SafetyCenterIssueKey.newBuilder()
+                                                .setSafetySourceId(UNUSED_APPS_SOURCE_ID)
+                                                .setSafetySourceIssueId(UNUSED_APPS_ISSUE_ID)
+                                                .setUserId(UserHandle.myUserId())
+                                                .build())
+                                        .setIssueTypeId(UNUSED_APPS_ISSUE_ID)
+                                        .build())
+                        assertTrue(issues.any { it.id == issueId })
+                    }
+                }
+            }
+        }
+    }
+
+    @AppModeFull(reason = "Uses separate apps for testing")
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+    @Test
+    fun testAutoRevoke_goToUnusedAppsPage_removesSafetyCenterIssue() {
+        assumeTrue(deviceSupportsSafetyCenter())
+        withSafetyCenterEnabled {
+            withUnusedThresholdMs(3L) {
+                withDummyApp {
+                    startAppAndAcceptPermission()
+
+                    killDummyApp()
+
+                    // Run
+                    runAppHibernationJob(context, LOG_TAG)
+
+                    // Go to unused apps page
+                    openUnusedAppsNotification()
+                    waitFindObject(By.text(supportedAppPackageName))
+
+                    // Verify
+                    val safetyCenterManager =
+                        context.getSystemService(SafetyCenterManager::class.java)!!
+                    eventually {
+                        val issues = ArrayList<SafetyCenterIssue>()
+                        runWithShellPermissionIdentity {
+                            val safetyCenterData = safetyCenterManager!!.safetyCenterData
+                            issues.addAll(safetyCenterData.issues)
+                        }
+                        val issueId = SafetyCenterIds.encodeToString(
+                                SafetyCenterIssueId.newBuilder()
+                                        .setSafetyCenterIssueKey(SafetyCenterIssueKey.newBuilder()
+                                                .setSafetySourceId(UNUSED_APPS_SOURCE_ID)
+                                                .setSafetySourceIssueId(UNUSED_APPS_ISSUE_ID)
+                                                .setUserId(UserHandle.myUserId())
+                                                .build())
+                                        .setIssueTypeId(UNUSED_APPS_ISSUE_ID)
+                                        .build())
+                        assertFalse(issues.any { it.id == issueId })
+                    }
+                }
+            }
+        }
+    }
+
     private fun isAutomotiveDevice(): Boolean {
         return context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
     }
 
+    private fun deviceSupportsSafetyCenter(): Boolean {
+        return context.resources.getBoolean(
+            Resources.getSystem().getIdentifier("config_enableSafetyCenter", "bool", "android"))
+    }
+
     private fun installApp() {
         installApk(supportedApkPath)
     }
@@ -456,6 +614,7 @@
             waitFindObject(By.res("com.android.permissioncontroller:id/permission_allow_button"))
                     .click()
         }
+        waitForIdle()
     }
 
     private fun clickUninstallIcon() {
diff --git a/tests/tests/media/codec/src/android/media/codec/cts/DecodeEditEncodeTest.java b/tests/tests/media/codec/src/android/media/codec/cts/DecodeEditEncodeTest.java
index 14f8516..e6c186c 100644
--- a/tests/tests/media/codec/src/android/media/codec/cts/DecodeEditEncodeTest.java
+++ b/tests/tests/media/codec/src/android/media/codec/cts/DecodeEditEncodeTest.java
@@ -25,6 +25,7 @@
 import android.media.cts.OutputSurface;
 import android.media.cts.TestArgs;
 import android.opengl.GLES20;
+import android.os.Build;
 import android.util.Log;
 
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -39,6 +40,7 @@
 import java.util.Collection;
 import java.util.List;
 
+import com.android.compatibility.common.util.ApiLevelUtil;
 import com.android.compatibility.common.util.MediaUtils;
 
 import org.junit.Before;
@@ -71,6 +73,7 @@
     private static final boolean WORK_AROUND_BUGS = false;  // avoid fatal codec bugs
     private static final boolean VERBOSE = false;           // lots of logging
     private static final boolean DEBUG_SAVE_FILE = false;   // save copy of encoded movie
+    private static final boolean IS_AFTER_T = ApiLevelUtil.isAfter(Build.VERSION_CODES.TIRAMISU);
 
     // parameters for the encoder
     private static final int FRAME_RATE = 15;               // 15fps
@@ -160,13 +163,18 @@
     }
 
     @Before
-    public void shouldSkip() {
+    public void shouldSkip() throws IOException {
         MediaFormat format = MediaFormat.createVideoFormat(mMediaType, mWidth, mHeight);
         format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
         format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
         format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
         assumeTrue(MediaUtils.supports(mEncoderName, format));
         assumeTrue(MediaUtils.supports(mDecoderName, format));
+        // Few cuttlefish specific color conversion issues were fixed after Android T.
+        if (MediaUtils.onCuttlefish()) {
+            assumeTrue("Color conversion related tests are not valid on cuttlefish releases "
+                    + "through android T for format: " + format, IS_AFTER_T);
+        }
     }
 
     @Parameterized.Parameters(name = "{index}({0}_{1}_{2}_{3}_{4})")
diff --git a/tests/tests/media/codec/src/android/media/codec/cts/EncodeDecodeTest.java b/tests/tests/media/codec/src/android/media/codec/cts/EncodeDecodeTest.java
index 7780a13..eff1f51 100644
--- a/tests/tests/media/codec/src/android/media/codec/cts/EncodeDecodeTest.java
+++ b/tests/tests/media/codec/src/android/media/codec/cts/EncodeDecodeTest.java
@@ -30,6 +30,7 @@
 import android.media.cts.SdkMediaCodec;
 import android.media.cts.TestArgs;
 import android.opengl.GLES20;
+import android.os.Build;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresDevice;
 import android.util.Log;
@@ -37,6 +38,7 @@
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SmallTest;
 
+import com.android.compatibility.common.util.ApiLevelUtil;
 import com.android.compatibility.common.util.MediaUtils;
 
 import java.io.FileOutputStream;
@@ -82,6 +84,7 @@
     private static final boolean VERBOSE = false;           // lots of logging
     private static final boolean DEBUG_SAVE_FILE = false;   // save copy of encoded movie
     private static final String DEBUG_FILE_NAME_BASE = "/sdcard/test.";
+    private static final boolean IS_AFTER_T = ApiLevelUtil.isAfter(Build.VERSION_CODES.TIRAMISU);
 
     // parameters for the encoder
     private static final int FRAME_RATE = 15;               // 15fps
@@ -283,6 +286,11 @@
          */
         public static void runTest(EncodeDecodeTest obj, boolean persisent, boolean useNdk)
                 throws Throwable {
+            // Few cuttlefish specific color conversion issues were fixed after Android T.
+            if (MediaUtils.onCuttlefish()) {
+                assumeTrue("Color conversion related tests are not valid on cuttlefish releases "
+                        + "through android T", IS_AFTER_T);
+            }
             SurfaceToSurfaceWrapper wrapper =
                     new SurfaceToSurfaceWrapper(obj, persisent, useNdk);
             Thread th = new Thread(wrapper, "codec test");
diff --git a/tests/tests/media/codec/src/android/media/codec/cts/ExtractDecodeEditEncodeMuxTest.java b/tests/tests/media/codec/src/android/media/codec/cts/ExtractDecodeEditEncodeMuxTest.java
index 12b5174..0ae09d2 100644
--- a/tests/tests/media/codec/src/android/media/codec/cts/ExtractDecodeEditEncodeMuxTest.java
+++ b/tests/tests/media/codec/src/android/media/codec/cts/ExtractDecodeEditEncodeMuxTest.java
@@ -42,6 +42,8 @@
 import android.media.MediaCodecInfo.CodecCapabilities;
 import android.media.MediaCodecInfo.CodecProfileLevel;
 
+import com.android.compatibility.common.util.CddTest;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -130,38 +132,43 @@
         super(MediaStubActivity.class);
     }
 
+    @CddTest(requirements = {"5.2", "5.3"})
     public void testExtractDecodeEditEncodeMuxQCIF() throws Throwable {
         if(!setSize(176, 144)) return;
         setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
         setCopyVideo();
-        setVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
+        setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
         TestWrapper.runTest(this);
     }
 
+    @CddTest(requirements = {"5.2", "5.3"})
     public void testExtractDecodeEditEncodeMuxQVGA() throws Throwable {
         if(!setSize(320, 240)) return;
         setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
         setCopyVideo();
-        setVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
+        setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
         TestWrapper.runTest(this);
     }
 
+    @CddTest(requirements = {"5.2", "5.3"})
     public void testExtractDecodeEditEncodeMux720p() throws Throwable {
         if(!setSize(1280, 720)) return;
         setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
         setCopyVideo();
-        setVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
+        setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
         TestWrapper.runTest(this);
     }
 
+    @CddTest(requirements = {"5.2", "5.3"})
     public void testExtractDecodeEditEncodeMux2160pHevc() throws Throwable {
         if(!setSize(3840, 2160)) return;
         setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
         setCopyVideo();
-        setVideoMimeType(MediaFormat.MIMETYPE_VIDEO_HEVC);
+        setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_HEVC);
         TestWrapper.runTest(this);
     }
 
+    @CddTest(requirements = {"5.1.1", "5.1.2"})
     public void testExtractDecodeEditEncodeMuxAudio() throws Throwable {
         if(!setSize(1280, 720)) return;
         setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
@@ -170,11 +177,13 @@
         TestWrapper.runTest(this);
     }
 
+    @CddTest(requirements = {"5.1.1", "5.1.2", "5.2", "5.3"})
     public void testExtractDecodeEditEncodeMuxAudioVideo() throws Throwable {
         if(!setSize(1280, 720)) return;
         setSource("video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4");
         setCopyAudio();
         setCopyVideo();
+        setOutputVideoMimeType(MediaFormat.MIMETYPE_VIDEO_AVC);
         setVerifyAudioFormat();
         TestWrapper.runTest(this);
     }
@@ -289,7 +298,7 @@
         mOutputFile = sb.toString();
     }
 
-    private void setVideoMimeType(String mimeType) {
+    private void setOutputVideoMimeType(String mimeType) {
         mOutputVideoMimeType = mimeType;
     }
 
@@ -305,44 +314,52 @@
 
         MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
 
-        // We avoid the device-specific limitations on width and height by using values
-        // that are multiples of 16, which all tested devices seem to be able to handle.
-        MediaFormat outputVideoFormat =
-                MediaFormat.createVideoFormat(mOutputVideoMimeType, mWidth, mHeight);
+        String videoEncoderName = null;
+        MediaFormat outputVideoFormat = null;
+        if (mCopyVideo) {
+            // We avoid the device-specific limitations on width and height by using values
+            // that are multiples of 16, which all tested devices seem to be able to handle.
+            outputVideoFormat =
+                    MediaFormat.createVideoFormat(mOutputVideoMimeType, mWidth, mHeight);
 
-        // Set some properties. Failing to specify some of these can cause the MediaCodec
-        // configure() call to throw an unhelpful exception.
-        outputVideoFormat.setInteger(
-                MediaFormat.KEY_COLOR_FORMAT, OUTPUT_VIDEO_COLOR_FORMAT);
-        outputVideoFormat.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_VIDEO_BIT_RATE);
-        outputVideoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, OUTPUT_VIDEO_FRAME_RATE);
-        outputVideoFormat.setInteger(
-                MediaFormat.KEY_I_FRAME_INTERVAL, OUTPUT_VIDEO_IFRAME_INTERVAL);
-        if (VERBOSE) Log.d(TAG, "video format: " + outputVideoFormat);
+            // Set some properties. Failing to specify some of these can cause the MediaCodec
+            // configure() call to throw an unhelpful exception.
+            outputVideoFormat.setInteger(
+                    MediaFormat.KEY_COLOR_FORMAT, OUTPUT_VIDEO_COLOR_FORMAT);
+            outputVideoFormat.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_VIDEO_BIT_RATE);
+            outputVideoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, OUTPUT_VIDEO_FRAME_RATE);
+            outputVideoFormat.setInteger(
+                    MediaFormat.KEY_I_FRAME_INTERVAL, OUTPUT_VIDEO_IFRAME_INTERVAL);
+            if (VERBOSE) Log.d(TAG, "video format: " + outputVideoFormat);
 
-        String videoEncoderName = mcl.findEncoderForFormat(outputVideoFormat);
-        if (videoEncoderName == null) {
-            // Don't fail CTS if they don't have an AVC codec (not here, anyway).
-            Log.e(TAG, "Unable to find an appropriate codec for " + outputVideoFormat);
-            return;
+            videoEncoderName = mcl.findEncoderForFormat(outputVideoFormat);
+            if (videoEncoderName == null) {
+                // Don't fail CTS if they don't have an AVC codec (not here, anyway).
+                Log.e(TAG, "Unable to find an appropriate codec for " + outputVideoFormat);
+                return;
+            }
+            if (VERBOSE) Log.d(TAG, "video found codec: " + videoEncoderName);
         }
-        if (VERBOSE) Log.d(TAG, "video found codec: " + videoEncoderName);
 
-        MediaFormat outputAudioFormat =
-                MediaFormat.createAudioFormat(
-                        OUTPUT_AUDIO_MIME_TYPE, OUTPUT_AUDIO_SAMPLE_RATE_HZ,
-                        OUTPUT_AUDIO_CHANNEL_COUNT);
-        outputAudioFormat.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_AUDIO_BIT_RATE);
-        // TODO: Bug workaround --- uncomment once fixed.
-        // outputAudioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, OUTPUT_AUDIO_AAC_PROFILE);
+        String audioEncoderName = null;
+        MediaFormat outputAudioFormat = null;
+        if (mCopyAudio) {
+            outputAudioFormat =
+                    MediaFormat.createAudioFormat(
+                            OUTPUT_AUDIO_MIME_TYPE, OUTPUT_AUDIO_SAMPLE_RATE_HZ,
+                            OUTPUT_AUDIO_CHANNEL_COUNT);
+            outputAudioFormat.setInteger(MediaFormat.KEY_BIT_RATE, OUTPUT_AUDIO_BIT_RATE);
+            // TODO: Bug workaround --- uncomment once fixed.
+            // outputAudioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, OUTPUT_AUDIO_AAC_PROFILE);
 
-        String audioEncoderName = mcl.findEncoderForFormat(outputAudioFormat);
-        if (audioEncoderName == null) {
-            // Don't fail CTS if they don't have an AAC codec (not here, anyway).
-            Log.e(TAG, "Unable to find an appropriate codec for " + outputAudioFormat);
-            return;
+            audioEncoderName = mcl.findEncoderForFormat(outputAudioFormat);
+            if (audioEncoderName == null) {
+                // Don't fail CTS if they don't have an AAC codec (not here, anyway).
+                Log.e(TAG, "Unable to find an appropriate codec for " + outputAudioFormat);
+                return;
+            }
+            if (VERBOSE) Log.d(TAG, "audio found codec: " + audioEncoderName);
         }
-        if (VERBOSE) Log.d(TAG, "audio found codec: " + audioEncoderName);
 
         MediaExtractor videoExtractor = null;
         MediaExtractor audioExtractor = null;
@@ -863,29 +880,25 @@
                 ByteBuffer decoderInputBuffer = audioDecoderInputBuffers[decoderInputBufferIndex];
                 int size = audioExtractor.readSampleData(decoderInputBuffer, 0);
                 long presentationTime = audioExtractor.getSampleTime();
+                int flags = audioExtractor.getSampleFlags();
                 if (VERBOSE) {
                     Log.d(TAG, "audio extractor: returned buffer of size " + size);
                     Log.d(TAG, "audio extractor: returned buffer for time " + presentationTime);
                 }
+                audioExtractorDone = !audioExtractor.advance();
+                if (audioExtractorDone) {
+                    if (VERBOSE) Log.d(TAG, "audio extractor: EOS");
+                    flags = flags | MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+                }
                 if (size >= 0) {
                     audioDecoder.queueInputBuffer(
                             decoderInputBufferIndex,
                             0,
                             size,
                             presentationTime,
-                            audioExtractor.getSampleFlags());
+                            flags);
+                    audioExtractedFrameCount++;
                 }
-                audioExtractorDone = !audioExtractor.advance();
-                if (audioExtractorDone) {
-                    if (VERBOSE) Log.d(TAG, "audio extractor: EOS");
-                    audioDecoder.queueInputBuffer(
-                            decoderInputBufferIndex,
-                            0,
-                            0,
-                            0,
-                            MediaCodec.BUFFER_FLAG_END_OF_STREAM);
-                }
-                audioExtractedFrameCount++;
                 // We extracted a frame, let's try something else next.
                 break;
             }
diff --git a/tests/tests/media/common/src/android/media/cts/TestUtils.java b/tests/tests/media/common/src/android/media/cts/TestUtils.java
index f98b3ab..552cf65 100644
--- a/tests/tests/media/common/src/android/media/cts/TestUtils.java
+++ b/tests/tests/media/common/src/android/media/cts/TestUtils.java
@@ -187,6 +187,21 @@
             return true;
         }
         // MTS mode, just the codecs that live in the modules
+        if (isMainlineCodec(name)) {
+            return true;
+        }
+        Log.d(TAG, "Test mode MTS does not test codec " + name);
+        return false;
+    }
+
+    /*
+     * Report whether this codec is a google-supplied codec that lives within the
+     * mainline modules.
+     *
+     * @param name    the name of a codec
+     * @return {@code} true if the codec is one that lives within the mainline boundaries
+     */
+    public static boolean isMainlineCodec(String name) {
         if (name.startsWith("c2.android.")) {
             return true;
         }
diff --git a/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeAccuracyTest.java b/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeAccuracyTest.java
index c982376..1a1280c 100644
--- a/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeAccuracyTest.java
+++ b/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeAccuracyTest.java
@@ -27,6 +27,8 @@
 import android.media.MediaFormat;
 import android.media.cts.MediaHeavyPresubmitTest;
 import android.media.cts.TestArgs;
+import android.media.cts.TestUtils;
+import android.os.Build;
 import android.os.Environment;
 import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
@@ -34,6 +36,7 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.ApiLevelUtil;
 import com.android.compatibility.common.util.MediaUtils;
 
 import java.io.File;
@@ -64,6 +67,10 @@
 @AppModeFull(reason = "There should be no instant apps specific behavior related to accuracy")
 public class DecodeAccuracyTest extends DecodeAccuracyTestBase {
 
+    private static final boolean IS_AT_LEAST_U = ApiLevelUtil.isAfter(Build.VERSION_CODES.TIRAMISU)
+            || ApiLevelUtil.codenameEquals("UpsideDownCake");
+    private static final boolean IS_BEFORE_U = !IS_AT_LEAST_U;
+
     private static final String TAG = DecodeAccuracyTest.class.getSimpleName();
     private static final Field[] fields = R.raw.class.getFields();
     private static final int ALLOWED_GREATEST_PIXEL_DIFFERENCE = 90;
@@ -281,7 +288,7 @@
         final int golden = getGoldenId(vf.getDescription(), vf.getOriginalSize());
         assertTrue("No golden found.", golden != 0);
         decodeVideo(vf, videoViewFactory, decoderName);
-        validateResult(vf, videoViewFactory.getVideoViewSnapshot(), golden);
+        validateResult(vf, videoViewFactory.getVideoViewSnapshot(), golden, decoderName);
     }
 
     private void decodeVideo(VideoFormat videoFormat, VideoViewFactory videoViewFactory,
@@ -293,12 +300,24 @@
     }
 
     private void validateResult(
-            VideoFormat videoFormat, VideoViewSnapshot videoViewSnapshot, int goldenId) {
+            VideoFormat videoFormat, VideoViewSnapshot videoViewSnapshot, int goldenId,
+            String decoderName) {
         final Bitmap result = checkNotNull("The expected bitmap from snapshot is null",
                 getHelper().generateBitmapFromVideoViewSnapshot(videoViewSnapshot));
         final Bitmap golden = getHelper().generateBitmapFromImageResourceId(goldenId);
+
+        int ignorePixels = 0;
+        if (IS_BEFORE_U && TestUtils.isMtsMode()) {
+            if (TestUtils.isMainlineCodec(decoderName)) {
+                // some older systems don't give proper behavior at the edges (in system code).
+                // while we can't fix the behavior at the edges, we can verify that the rest
+                // of the image is within tolerance. b/256807044
+                ignorePixels = 1;
+            }
+        }
         final BitmapCompare.Difference difference = BitmapCompare.computeMinimumDifference(
-                result, golden, videoFormat.getOriginalWidth(), videoFormat.getOriginalHeight());
+                result, golden, ignorePixels, videoFormat.getOriginalWidth(),
+                videoFormat.getOriginalHeight());
 
         if (difference.greatestPixelDifference > ALLOWED_GREATEST_PIXEL_DIFFERENCE) {
             /* save failing file */
diff --git a/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeAccuracyTestBase.java b/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeAccuracyTestBase.java
index d26c45f..38fdb18 100644
--- a/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeAccuracyTestBase.java
+++ b/tests/tests/media/decoder/src/android/media/decoder/cts/DecodeAccuracyTestBase.java
@@ -1763,6 +1763,7 @@
  * CIE L*a*b* color space. The euclidean distance formula is used to determine pixel differences.
  */
 class BitmapCompare {
+    private static final String TAG = "BitmapCompare";
 
     private static final int RED = 0;
     private static final int GREEN = 1;
@@ -1776,6 +1777,8 @@
     /**
      * Produces greatest pixel between two bitmaps. Used to determine bitmap similarity.
      *
+     * This simplified variant does not ignore any edge pixels.
+     *
      * @param bitmap1 A bitmap to compare to bitmap2.
      * @param bitmap2 A bitmap to compare to bitmap1.
      * @return A {@link Difference} with an integer describing the greatest pixel difference,
@@ -1783,7 +1786,26 @@
      *     {@link Pair<Integer, Integer>} of the (col, row) pixel coordinate where it was first found.
      */
     @TargetApi(12)
-    public static Difference computeDifference(Bitmap bitmap1, Bitmap bitmap2) {
+    private static Difference computeDifference(Bitmap bitmap1, Bitmap bitmap2) {
+        return computeDifference(bitmap1, bitmap2, 0);
+    }
+
+    /**
+     * Produces greatest pixel between two bitmaps. Used to determine bitmap similarity.
+     *
+     * @param bitmap1 A bitmap to compare to bitmap2.
+     * @param bitmap2 A bitmap to compare to bitmap1.
+     * @param ignorePixels number of pixels at each edge where we ignore the scoring. This
+     *     is used for mainline and older base systems to bypass an edge behavior in the
+     *     GPU code on those systems.
+     * @return A {@link Difference} with an integer describing the greatest pixel difference,
+     *     using {@link Integer#MAX_VALUE} for completely different bitmaps, and an optional
+     *     {@link Pair<Integer, Integer>} of the (col, row) pixel coordinate where it was
+     *     first found.
+     */
+    @TargetApi(12)
+    private static Difference computeDifference(Bitmap bitmap1, Bitmap bitmap2, int ignorePixels) {
+        Log.i(TAG, "ignorePixels = " + ignorePixels);
         if (bitmap1 == null || bitmap2 == null) {
             return new Difference(Integer.MAX_VALUE);
         }
@@ -1797,15 +1819,31 @@
         // euclidean distance formula.
         final double[][] pixels1 = convertRgbToCieLab(bitmap1);
         final double[][] pixels2 = convertRgbToCieLab(bitmap2);
-        int greatestDifference = 0;
+        int greatestDifference = -1;    // forces a legal index later...
         int greatestDifferenceIndex = -1;
         for (int i = 0; i < pixels1.length; i++) {
+            // pixels within 'ignorePixels' of the edge are to be ignored for
+            // scoring purposes.
+            int x = i % bitmap1.getWidth();
+            int y = i / bitmap1.getWidth();
+            if (x < ignorePixels || x >= bitmap1.getWidth() - ignorePixels
+                    || y < ignorePixels || y >= bitmap1.getHeight() - ignorePixels) {
+                continue;
+            }
+
             final int difference = euclideanDistance(pixels1[i], pixels2[i]);
+
             if (difference > greatestDifference) {
                 greatestDifference = difference;
                 greatestDifferenceIndex = i;
             }
         }
+
+        // huge ignorePixels values can get here without checking any pixels
+        if (greatestDifferenceIndex == -1) {
+            greatestDifferenceIndex = 0;
+            greatestDifference = 0;
+        }
         return new Difference(greatestDifference, Pair.create(
             greatestDifferenceIndex % bitmap1.getWidth(),
             greatestDifferenceIndex / bitmap1.getWidth()));
@@ -2019,16 +2057,16 @@
      */
     @TargetApi(12)
     public static Difference computeMinimumDifference(
-            Bitmap bitmap1, Bitmap bitmap2, Pair<Double, Double>[] borderCrops) {
+            Bitmap bitmap1, Bitmap bitmap2, int ignorePixels, Pair<Double, Double>[] borderCrops) {
 
         // Compute the difference with the original image (bitmap2) first.
-        Difference minDiff = computeDifference(bitmap1, bitmap2);
+        Difference minDiff = computeDifference(bitmap1, bitmap2, ignorePixels);
         // Then go through the list of borderCrops.
         for (Pair<Double, Double> borderCrop : borderCrops) {
             // Compute the difference between bitmap1 and a transformed
             // version of bitmap2.
             Bitmap bitmap2s = shrinkAndScaleBilinear(bitmap2, borderCrop.first, borderCrop.second);
-            Difference d = computeDifference(bitmap1, bitmap2s);
+            Difference d = computeDifference(bitmap1, bitmap2s, ignorePixels);
             // Keep the minimum difference.
             if (d.greatestPixelDifference < minDiff.greatestPixelDifference) {
                 minDiff = d;
@@ -2043,7 +2081,7 @@
      */
     @TargetApi(12)
     public static Difference computeMinimumDifference(
-            Bitmap bitmap1, Bitmap bitmap2, int trueWidth, int trueHeight) {
+            Bitmap bitmap1, Bitmap bitmap2, int ignorePixels, int trueWidth, int trueHeight) {
 
         double hBorder = (double) bitmap1.getWidth() / (double) trueWidth;
         double vBorder = (double) bitmap1.getHeight() / (double) trueHeight;
@@ -2052,6 +2090,7 @@
         return computeMinimumDifference(
                 bitmap1,
                 bitmap2,
+                ignorePixels,
                 new Pair[] {
                     Pair.create(hBorderH, 0.0),
                     Pair.create(hBorderH, vBorderH),
diff --git a/tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java b/tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java
index 7eca1ad..b6d3131 100644
--- a/tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java
+++ b/tests/tests/media/decoder/src/android/media/decoder/cts/DecoderTest.java
@@ -59,6 +59,7 @@
 import android.media.cts.NonMediaMainlineTest;
 import android.media.cts.Preconditions;
 import android.media.cts.SdkMediaCodec;
+import android.media.cts.TestUtils;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -122,6 +123,8 @@
                     >= Build.VERSION_CODES.S;
 
     private static boolean IS_AT_LEAST_R = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R);
+    private static boolean IS_BEFORE_S = ApiLevelUtil.isBefore(Build.VERSION_CODES.S);
+    private static boolean IS_AFTER_T = ApiLevelUtil.isAfter(Build.VERSION_CODES.TIRAMISU);
 
     private static final int RESET_MODE_NONE = 0;
     private static final int RESET_MODE_RECONFIGURE = 1;
@@ -2074,6 +2077,11 @@
 
     protected static List<String> codecsFor(String resource, int codecSupportMode)
             throws IOException {
+
+        // CODEC_DEFAULT behaviors started with S
+        if (IS_BEFORE_S) {
+            codecSupportMode = CODEC_ALL;
+        }
         MediaExtractor ex = new MediaExtractor();
         AssetFileDescriptor fd = getAssetFileDescriptorFor(resource);
         try {
@@ -2093,12 +2101,24 @@
             try {
                 MediaCodecInfo.CodecCapabilities caps = info.getCapabilitiesForType(mime);
                 if (caps != null) {
+                    // do we test this codec in current mode?
+                    if (!TestUtils.isTestableCodecInCurrentMode(info.getName())) {
+                        Log.i(TAG, "skip codec " + info.getName() + " in current mode");
+                        continue;
+                    }
                     if (codecSupportMode == CODEC_ALL) {
+                        if (IS_AFTER_T) {
+                            // This is an extractor failure as often as it is a codec failure
+                            assertTrue(info.getName() + " does not declare support for "
+                                    + format.toString(),
+                                    caps.isFormatSupported(format));
+                        }
                         matchingCodecs.add(info.getName());
                     } else if (codecSupportMode == CODEC_DEFAULT) {
                         if (caps.isFormatSupported(format)) {
                             matchingCodecs.add(info.getName());
                         } else if (isDefaultCodec(info.getName(), mime)) {
+                            // This is an extractor failure as often as it is a codec failure
                             fail(info.getName() + " which is a default decoder for mime " + mime
                                    + ", does not declare support for " + format.toString());
                         }
@@ -2110,7 +2130,15 @@
                 // type is not supported
             }
         }
-        assertTrue("no matching codecs found", matchingCodecs.size() != 0);
+        if (TestUtils.isMtsMode()) {
+            // not fatal in MTS mode
+            Assume.assumeTrue("no MTS-mode codecs found for format " + format.toString(),
+                            matchingCodecs.size() != 0);
+        } else {
+            // but fatal in CTS mode
+            assertTrue("no codecs found for format " + format.toString(),
+                            matchingCodecs.size() != 0);
+        }
         return matchingCodecs;
     }
 
diff --git a/tests/tests/media/decoder/src/android/media/decoder/cts/NativeDecoderTest.java b/tests/tests/media/decoder/src/android/media/decoder/cts/NativeDecoderTest.java
index f5e3e0d..9b98fed 100644
--- a/tests/tests/media/decoder/src/android/media/decoder/cts/NativeDecoderTest.java
+++ b/tests/tests/media/decoder/src/android/media/decoder/cts/NativeDecoderTest.java
@@ -129,81 +129,6 @@
         return new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize());
     }
 
-    private static int[] getSampleSizes(String path, String[] keys, String[] values)
-            throws IOException {
-        MediaExtractor ex = new MediaExtractor();
-        if (keys == null || values == null) {
-            ex.setDataSource(path);
-        } else {
-            Map<String, String> headers = null;
-            int numheaders = Math.min(keys.length, values.length);
-            for (int i = 0; i < numheaders; i++) {
-                if (headers == null) {
-                    headers = new HashMap<>();
-                }
-                String key = keys[i];
-                String value = values[i];
-                headers.put(key, value);
-            }
-            ex.setDataSource(path, headers);
-        }
-
-        return getSampleSizes(ex);
-    }
-
-    private static int[] getSampleSizes(FileDescriptor fd, long offset, long size)
-            throws IOException {
-        MediaExtractor ex = new MediaExtractor();
-        ex.setDataSource(fd, offset, size);
-        return getSampleSizes(ex);
-    }
-
-    private static int[] getSampleSizes(MediaExtractor ex) {
-        ArrayList<Integer> foo = new ArrayList<Integer>();
-        ByteBuffer buf = ByteBuffer.allocate(1024*1024);
-        int numtracks = ex.getTrackCount();
-        assertTrue("no tracks", numtracks > 0);
-        foo.add(numtracks);
-        for (int i = 0; i < numtracks; i++) {
-            MediaFormat format = ex.getTrackFormat(i);
-            String mime = format.getString(MediaFormat.KEY_MIME);
-            if (mime.startsWith("audio/")) {
-                foo.add(0);
-                foo.add(format.getInteger(MediaFormat.KEY_SAMPLE_RATE));
-                foo.add(format.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
-                foo.add((int)format.getLong(MediaFormat.KEY_DURATION));
-            } else if (mime.startsWith("video/")) {
-                foo.add(1);
-                foo.add(format.getInteger(MediaFormat.KEY_WIDTH));
-                foo.add(format.getInteger(MediaFormat.KEY_HEIGHT));
-                foo.add((int)format.getLong(MediaFormat.KEY_DURATION));
-            } else {
-                fail("unexpected mime type: " + mime);
-            }
-            ex.selectTrack(i);
-        }
-        while(true) {
-            int n = ex.readSampleData(buf, 0);
-            if (n < 0) {
-                break;
-            }
-            foo.add(n);
-            foo.add(ex.getSampleTrackIndex());
-            foo.add(ex.getSampleFlags());
-            foo.add((int)ex.getSampleTime()); // just the low bits should be OK
-            byte[] foobar = new byte[n];
-            buf.get(foobar, 0, n);
-            foo.add(adler32(foobar));
-            ex.advance();
-        }
-
-        int [] ret = new int[foo.size()];
-        for (int i = 0; i < ret.length; i++) {
-            ret[i] = foo.get(i);
-        }
-        return ret;
-    }
-
     @Test
     public void testDataSource() throws Exception {
         int testsRun = testDecoder(
diff --git a/tests/tests/media/decoder/src/android/media/decoder/cts/VideoDecoderPerfTest.java b/tests/tests/media/decoder/src/android/media/decoder/cts/VideoDecoderPerfTest.java
index ef7646b..3db3d57 100644
--- a/tests/tests/media/decoder/src/android/media/decoder/cts/VideoDecoderPerfTest.java
+++ b/tests/tests/media/decoder/src/android/media/decoder/cts/VideoDecoderPerfTest.java
@@ -211,7 +211,7 @@
         }
 
         // allow improvements in mainline-updated google-supplied software codecs.
-        boolean fasterIsOk = mUpdatedSwCodec & name.startsWith("c2.android.");
+        boolean fasterIsOk = mUpdatedSwCodec & TestUtils.isMainlineCodec(name);
         String error =
             MediaPerfUtils.verifyAchievableFrameRates(name, mime, width, height,
                            fasterIsOk,  measuredFps);
diff --git a/tests/tests/media/encoder/src/android/media/encoder/cts/VideoEncoderTest.java b/tests/tests/media/encoder/src/android/media/encoder/cts/VideoEncoderTest.java
index 4afc7aa..74aa214 100644
--- a/tests/tests/media/encoder/src/android/media/encoder/cts/VideoEncoderTest.java
+++ b/tests/tests/media/encoder/src/android/media/encoder/cts/VideoEncoderTest.java
@@ -43,6 +43,7 @@
 import android.media.cts.OutputSurface;
 import android.media.cts.Preconditions;
 import android.media.cts.TestArgs;
+import android.media.cts.TestUtils;
 import android.net.Uri;
 import android.platform.test.annotations.AppModeFull;
 import android.util.Log;
@@ -1301,6 +1302,10 @@
                 if (TestArgs.shouldSkipCodec(encoder)) {
                     continue;
                 }
+                if (!TestUtils.isTestableCodecInCurrentMode(encoder)) {
+                    Log.d(TAG, "Skipping tests for codec: " + encoder);
+                    continue;
+                }
                 CodecCapabilities caps = getCodecCapabities(encoder, mediaType, true);
                 assertNotNull(caps);
                 EncoderSize encoderSize = new EncoderSize(encoder, mediaType, caps);
diff --git a/tests/tests/media/misc/AndroidTest.xml b/tests/tests/media/misc/AndroidTest.xml
index 4a2ffaf..1facb8d 100644
--- a/tests/tests/media/misc/AndroidTest.xml
+++ b/tests/tests/media/misc/AndroidTest.xml
@@ -40,7 +40,7 @@
     </target_preparer>
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.MediaPreparer">
         <option name="push-all" value="true" />
-        <option name="media-folder-name" value="CtsMediaMiscTestCases-1.0" />
+        <option name="media-folder-name" value="CtsMediaMiscTestCases-2.0" />
         <option name="dynamic-config-module" value="CtsMediaMiscTestCases" />
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
diff --git a/tests/tests/media/misc/DynamicConfig.xml b/tests/tests/media/misc/DynamicConfig.xml
index 09e5fba..51e05bb 100644
--- a/tests/tests/media/misc/DynamicConfig.xml
+++ b/tests/tests/media/misc/DynamicConfig.xml
@@ -15,6 +15,6 @@
 
 <dynamicConfig>
     <entry key="media_files_url">
-    <value>https://storage.googleapis.com/android_media/cts/tests/tests/media/misc/CtsMediaMiscTestCases-1.0.zip</value>
+    <value>https://storage.googleapis.com/android_media/cts/tests/tests/media/misc/CtsMediaMiscTestCases-2.0.zip</value>
     </entry>
 </dynamicConfig>
diff --git a/tests/tests/media/misc/copy_media.sh b/tests/tests/media/misc/copy_media.sh
index 96f5a49..27ba1e7 100755
--- a/tests/tests/media/misc/copy_media.sh
+++ b/tests/tests/media/misc/copy_media.sh
@@ -17,4 +17,4 @@
 [ -z "$MEDIA_ROOT_DIR" ] && MEDIA_ROOT_DIR=$(dirname $0)/..
 source $MEDIA_ROOT_DIR/common/copy_media_utils.sh
 get_adb_options "$@"
-copy_media "misc" "CtsMediaMiscTestCases-1.0"
+copy_media "misc" "CtsMediaMiscTestCases-2.0"
diff --git a/tests/tests/media/misc/src/android/media/misc/cts/WorkDir.java b/tests/tests/media/misc/src/android/media/misc/cts/WorkDir.java
index 0c39229..dbb6035 100644
--- a/tests/tests/media/misc/src/android/media/misc/cts/WorkDir.java
+++ b/tests/tests/media/misc/src/android/media/misc/cts/WorkDir.java
@@ -20,6 +20,6 @@
 
 class WorkDir extends WorkDirBase {
     public static final String getMediaDirString() {
-        return getMediaDirString("CtsMediaMiscTestCases-1.0");
+        return getMediaDirString("CtsMediaMiscTestCases-2.0");
     }
 }
diff --git a/tests/tests/media/player/src/android/media/player/cts/MediaPlayerFlakyNetworkTest.java b/tests/tests/media/player/src/android/media/player/cts/MediaPlayerFlakyNetworkTest.java
index 81c0d89..86c6912 100644
--- a/tests/tests/media/player/src/android/media/player/cts/MediaPlayerFlakyNetworkTest.java
+++ b/tests/tests/media/player/src/android/media/player/cts/MediaPlayerFlakyNetworkTest.java
@@ -75,6 +75,7 @@
     private CtsTestServer mServer;
 
     @Before
+    @Override
     public void setUp() throws Throwable {
         super.setUp();
     }
diff --git a/tests/tests/mediastress/src/android/mediastress/cts/MediaRecorderStressTest.java b/tests/tests/mediastress/src/android/mediastress/cts/MediaRecorderStressTest.java
index 671d8ad..c7aad36 100644
--- a/tests/tests/mediastress/src/android/mediastress/cts/MediaRecorderStressTest.java
+++ b/tests/tests/mediastress/src/android/mediastress/cts/MediaRecorderStressTest.java
@@ -284,6 +284,10 @@
             mRecorder.reset();
             mRecorder.release();
             output.write(", " + i);
+            if (mRemoveVideo) {
+                removeRecodedVideo(filename);
+            }
+
         }
 
         output.write("\n\n");
@@ -376,6 +380,9 @@
             mRecorder.release();
             Log.v(TAG, "release video recorder");
             output.write(", " + i);
+            if (mRemoveVideo) {
+                removeRecodedVideo(filename);
+            }
         }
 
         output.write("\n\n");
diff --git a/tests/tests/os/Android.bp b/tests/tests/os/Android.bp
index 8bc8222..7645ce7 100644
--- a/tests/tests/os/Android.bp
+++ b/tests/tests/os/Android.bp
@@ -18,10 +18,7 @@
 
 android_test {
     name: "CtsOsTestCases",
-    defaults: [
-        "cts_defaults",
-        "mts-target-sdk-version-current",
-    ],
+    defaults: ["cts_defaults"],
     compile_multilib: "both",
     static_libs: [
         "android.hidl.manager-V1.0-java",
@@ -63,7 +60,6 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts-permission",
         "sts",
     ],
     sdk_version: "test_current",
@@ -75,9 +71,6 @@
     aaptflags: ["-0 .policy"],
     min_sdk_version: "29",
     data: [
-        ":CtsAutoRevokeQApp",
-        ":CtsAutoRevokeSApp",
-        ":CtsAutoRevokeRApp",
         ":CtsCompanionTestApp",
     ],
     per_testcase_directory: true,
diff --git a/tests/tests/os/CtsOsTestCases.xml b/tests/tests/os/CtsOsTestCases.xml
index 631b627..e0a13b1 100644
--- a/tests/tests/os/CtsOsTestCases.xml
+++ b/tests/tests/os/CtsOsTestCases.xml
@@ -48,9 +48,6 @@
     </target_preparer>
     <!-- Load additional APKs onto device -->
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
-        <option name="push" value="CtsAutoRevokeSApp.apk->/data/local/tmp/cts/os/CtsAutoRevokeSApp.apk" />
-        <option name="push" value="CtsAutoRevokeRApp.apk->/data/local/tmp/cts/os/CtsAutoRevokeRApp.apk" />
-        <option name="push" value="CtsAutoRevokeQApp.apk->/data/local/tmp/cts/os/CtsAutoRevokeQApp.apk" />
         <option name="push" value="CtsCompanionTestApp.apk->/data/local/tmp/cts/os/CtsCompanionTestApp.apk" />
     </target_preparer>
 </configuration>
diff --git a/tests/tests/os/OWNERS b/tests/tests/os/OWNERS
index 5c286aa..8ab70d2a 100644
--- a/tests/tests/os/OWNERS
+++ b/tests/tests/os/OWNERS
@@ -1,5 +1,3 @@
-per-file *AutoRevoke* = file:platform/frameworks/base:/core/java/android/permission/OWNERS
-per-file *AppHibernation* = file:platform/frameworks/base:/core/java/android/permission/OWNERS
 per-file *Companion* = file:platform/frameworks/base:/core/java/android/permission/OWNERS
 per-file *Vibrat* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS
 per-file *AndroidManifest.xml = file:platform/frameworks/base:/core/java/android/permission/OWNERS
diff --git a/tests/tests/os/src/android/os/cts/CompanionDeviceManagerTest.kt b/tests/tests/os/src/android/os/cts/CompanionDeviceManagerTest.kt
index b398aff..bcdecdb 100644
--- a/tests/tests/os/src/android/os/cts/CompanionDeviceManagerTest.kt
+++ b/tests/tests/os/src/android/os/cts/CompanionDeviceManagerTest.kt
@@ -16,8 +16,12 @@
 
 package android.os.cts
 
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
 import android.app.Instrumentation
+import android.app.UiAutomation
 import android.companion.CompanionDeviceManager
+import android.content.Context
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE
 import android.content.pm.PackageManager.FEATURE_COMPANION_DEVICE_SETUP
@@ -31,22 +35,30 @@
 import android.util.Size
 import android.util.SizeF
 import android.util.SparseArray
+import android.view.accessibility.AccessibilityNodeInfo
 import androidx.test.InstrumentationRegistry
 import androidx.test.runner.AndroidJUnit4
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.UiObject2
-import androidx.test.uiautomator.Until
+import android.support.test.uiautomator.By
+import android.support.test.uiautomator.BySelector
+import android.support.test.uiautomator.UiDevice
+import android.support.test.uiautomator.UiObject2
+import android.support.test.uiautomator.Until
 import com.android.compatibility.common.util.MatcherUtils.hasIdThat
+import com.android.compatibility.common.util.SystemUtil
 import com.android.compatibility.common.util.SystemUtil.getEventually
 import com.android.compatibility.common.util.SystemUtil.runShellCommand
 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
 import com.android.compatibility.common.util.ThrowingSupplier
-import com.android.compatibility.common.util.UiAutomatorUtils.waitFindObject
+import com.android.compatibility.common.util.UI_ROOT
+import com.android.compatibility.common.util.UiAutomatorUtils
 import com.android.compatibility.common.util.click
+import com.android.compatibility.common.util.depthFirstSearch
+import com.android.compatibility.common.util.textAsString
+import com.android.compatibility.common.util.uiDump
 import org.hamcrest.CoreMatchers.containsString
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers
 import org.junit.After
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertThat
@@ -58,6 +70,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import java.io.Serializable
+import java.util.concurrent.atomic.AtomicReference
+import java.util.regex.Pattern
 
 /**
  * Test for [CompanionDeviceManager]
@@ -226,14 +240,112 @@
                     MacAddress.fromString(macAddress), context.user)
         }, *permissions)
     }
-}
 
-private fun UiDevice.waitAndFind(selector: BySelector): UiObject2 =
-        wait(Until.findObject(selector), 1000)
+    private fun UiDevice.waitAndFind(selector: BySelector): UiObject2 =
+            wait(Until.findObject(selector), 1000)
 
-private fun click(label: String) {
-    waitFindObject(byTextIgnoreCase(label)).click()
-    waitForIdle()
+    private fun click(label: String) {
+        waitFindObject(byTextIgnoreCase(label)).click()
+        waitForIdle()
+    }
+
+    private fun uninstallAppWithoutAssertion(packageName: String) {
+        runShellCommandOrThrow("pm uninstall $packageName")
+    }
+
+    private fun installApk(apk: String) {
+        assertThat(runShellCommandOrThrow("pm install -r $apk"), containsString("Success"))
+    }
+
+    /**
+     * For some reason waitFindObject sometimes fails to find UI that is present in the view hierarchy
+     */
+    private fun waitFindNode(
+            matcher: Matcher<AccessibilityNodeInfo>,
+            failMsg: String? = null,
+            timeoutMs: Long = 10_000
+    ): AccessibilityNodeInfo {
+        return getEventually({
+            val ui = UI_ROOT
+            ui.depthFirstSearch { node ->
+                matcher.matches(node)
+            }.assertNotNull {
+                buildString {
+                    if (failMsg != null) {
+                        appendLine(failMsg)
+                    }
+                    appendLine("No view found matching $matcher:\n\n${uiDump(ui)}")
+                }
+            }
+        }, timeoutMs)
+    }
+
+    private fun waitFindObject(selector: BySelector): UiObject2 {
+        return waitFindObject(instrumentation.uiAutomation, selector)
+    }
+
+    private fun waitFindObject(uiAutomation: UiAutomation, selector: BySelector): UiObject2 {
+        try {
+            return UiAutomatorUtils.waitFindObject(selector)
+        } catch (e: RuntimeException) {
+            val ui = uiAutomation.rootInActiveWindow
+
+            val title = ui.depthFirstSearch { node ->
+                node.viewIdResourceName?.contains("alertTitle") == true
+            }
+            val okButton = ui.depthFirstSearch { node ->
+                node.textAsString?.equals("OK", ignoreCase = true) ?: false
+            }
+
+            if (title?.text?.toString() == "Android System" && okButton != null) {
+                // Auto dismiss occasional system dialogs to prevent interfering with the test
+                okButton.click()
+                return UiAutomatorUtils.waitFindObject(selector)
+            } else {
+                throw e
+            }
+        }
+    }
+
+    private fun byTextIgnoreCase(txt: String): BySelector {
+        return By.text(Pattern.compile(txt, Pattern.CASE_INSENSITIVE))
+    }
+
+    private fun waitForIdle() {
+        InstrumentationRegistry.getInstrumentation().uiAutomation.waitForIdle(1000, 10000)
+    }
+
+    private inline fun <T> eventually(crossinline action: () -> T): T {
+        val res = AtomicReference<T>()
+        SystemUtil.eventually {
+            res.set(action())
+        }
+        return res.get()
+    }
+
+    private fun awaitAppState(pkg: String, stateMatcher: Matcher<Int>) {
+        val context: Context = InstrumentationRegistry.getTargetContext()
+        eventually {
+            runWithShellPermissionIdentity {
+                val packageImportance = context
+                        .getSystemService(ActivityManager::class.java)!!
+                        .getPackageImportance(pkg)
+                assertThat(packageImportance, stateMatcher)
+            }
+        }
+    }
+
+    private fun startApp(packageName: String) {
+        val context = InstrumentationRegistry.getTargetContext()
+        val intent = context.packageManager.getLaunchIntentForPackage(packageName)
+        context.startActivity(intent)
+        awaitAppState(packageName, Matchers.lessThanOrEqualTo(IMPORTANCE_TOP_SLEEPING))
+        waitForIdle()
+    }
+
+    private inline fun <T> T?.assertNotNull(errorMsg: () -> String): T {
+        return if (this == null) throw AssertionError(errorMsg()) else this
+    }
 }
 
 operator fun Bundle.set(key: String, value: Any?) {
diff --git a/tests/tests/os/src/android/os/storage/cts/StorageManagerCrossProfileSDCardTest.java b/tests/tests/os/src/android/os/storage/cts/StorageManagerCrossProfileSDCardTest.java
index f86ed9d..e823b1c 100644
--- a/tests/tests/os/src/android/os/storage/cts/StorageManagerCrossProfileSDCardTest.java
+++ b/tests/tests/os/src/android/os/storage/cts/StorageManagerCrossProfileSDCardTest.java
@@ -75,10 +75,12 @@
     public void testGetStorageVolumeSDCardWorkProfile() throws Exception {
         List<StorageVolume> storageVolumes = mStorageManager.getStorageVolumes();
         Optional<StorageVolume> sdCardStorageVolume =
-                storageVolumes.stream().filter(sv->sv.getPath().contains(mVolumeName)).findFirst();
+                storageVolumes.stream().filter(sv -> sv.getPath() != null && sv.getPath().contains(
+                        mVolumeName)).findFirst();
         assertWithMessage("The SdCard storage volume " + mVolumeName
                 + " mounted on the main user is present in "
-                + storageVolumes.stream().map(StorageVolume::getPath).collect(joining("\n")))
+                + storageVolumes.stream().filter(sv -> sv.getPath() != null).map(
+                StorageVolume::getPath).collect(joining("\n")))
                 .that(sdCardStorageVolume.isPresent()).isFalse();
     }
 }
diff --git a/tests/tests/permission/Android.bp b/tests/tests/permission/Android.bp
index f90f789..53263d4 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",
         "sts-device-util",
     ],
     jni_libs: [
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 95f79df..c50ccb4 100644
--- a/tests/tests/permission/AndroidTest.xml
+++ b/tests/tests/permission/AndroidTest.xml
@@ -94,6 +94,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" />
         <option name="push" value="CtsAppThatRequestsSystemAlertWindow22.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsSystemAlertWindow22.apk" />
         <option name="push" value="CtsAppThatRequestsSystemAlertWindow23.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestsSystemAlertWindow23.apk" />
     </target_preparer>
@@ -107,6 +108,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/os/AutoRevokeRApp/Android.bp b/tests/tests/permission/AppThatHasNotificationListener/Android.bp
similarity index 76%
copy from tests/tests/os/AutoRevokeRApp/Android.bp
copy to tests/tests/permission/AppThatHasNotificationListener/Android.bp
index 8e01388..419ab5d 100644
--- a/tests/tests/os/AutoRevokeRApp/Android.bp
+++ b/tests/tests/permission/AppThatHasNotificationListener/Android.bp
@@ -1,5 +1,5 @@
 //
-// Copyright (C) 2020 The Android Open Source Project
+// 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.
@@ -19,18 +19,21 @@
 }
 
 android_test_helper_app {
-    name: "CtsAutoRevokeRApp",
-    defaults: ["cts_defaults"],
-    sdk_version: "test_current",
+    name: "CtsAppThatHasNotificationListener",
+    defaults: [
+        "cts_defaults",
+        "mts-target-sdk-version-current",
+    ],
+    sdk_version: "current",
     min_sdk_version: "30",
-    target_sdk_version: "30",
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
-        "vts",
-        "mts-permission",
         "general-tests",
         "sts",
+        "mts-permission",
     ],
-    srcs: ["src/**/*.java", "src/**/*.kt"],
+    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/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt b/tests/tests/permission/AppThatHasNotificationListener/src/android/permission/cts/appthathasnotificationlistener/CtsNotificationListenerService.java
similarity index 60%
copy from tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
copy to tests/tests/permission/AppThatHasNotificationListener/src/android/permission/cts/appthathasnotificationlistener/CtsNotificationListenerService.java
index 101f200..2bd423e 100644
--- a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
+++ b/tests/tests/permission/AppThatHasNotificationListener/src/android/permission/cts/appthathasnotificationlistener/CtsNotificationListenerService.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,16 +14,8 @@
  * limitations under the License.
  */
 
-package android.os.cts.autorevokeqapp
+package android.permission.cts.appthathasnotificationlistener;
 
-import android.app.Activity
-import android.os.Bundle
+import android.service.notification.NotificationListenerService;
 
-class MainActivity : Activity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        requestPermissions(arrayOf("android.permission.READ_CALENDAR"), 0)
-    }
-}
+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..b88b9ae 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 static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+
+import android.app.UiAutomation;
+import android.os.Process;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Assert;
 
 /** Common test utilities */
 public class TestUtils {
@@ -123,4 +130,51 @@
             }
         }
     }
+
+    /**
+     * 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;
+        try {
+            runShellCommand(automation, runJobCmd);
+        } catch (Throwable e) {
+            throw new RuntimeException(e);
+        }
+        // waiting state is expected after completion for the periodic jobs.
+        awaitJobUntilRequestedState(packageName, jobId, timeout, automation, "waiting");
+    }
+
+    public static void awaitJobUntilRequestedState(
+            String packageName,
+            int jobId,
+            long timeout,
+            UiAutomation automation,
+            String requestedState) {
+        String statusCmd = "cmd jobscheduler get-job-state -u "
+                + Process.myUserHandle().getIdentifier() + " " + packageName + " " + jobId;
+        try {
+            eventually(() -> Assert.assertTrue(
+                    "The job doesn't have requested state " + requestedState + " yet",
+                    runShellCommand(automation, statusCmd).trim().startsWith(requestedState)),
+                    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..41a24db
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/AccessibilityPrivacySourceTest.kt
@@ -0,0 +1,319 @@
+/*
+ * 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.deleteDeviceConfigPrivacyProperty
+import android.permission.cts.SafetyCenterUtils.deviceSupportsSafetyCenter
+import android.permission.cts.SafetyCenterUtils.setDeviceConfigPrivacyProperty
+import android.platform.test.annotations.AppModeFull
+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.SystemUtil.runShellCommand
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+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)
+@AppModeFull(
+    reason = "Cannot set system settings as instant app. Also we never show an accessibility " +
+        "notification for instant apps."
+)
+@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 testJobSendsNotificationOnEnable() {
+        mAccessibilityServiceRule.enableService()
+        runJobAndWaitUntilCompleted()
+        assertNotificationExist(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+
+        setDeviceConfigPrivacyProperty(ACCESSIBILITY_LISTENER_ENABLED, true.toString())
+        cancelNotification(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+        InstrumentedAccessibilityService.disableAllServices()
+        setDeviceConfigPrivacyProperty(ACCESSIBILITY_LISTENER_ENABLED, false.toString())
+        setDeviceConfigPrivacyProperty(ACCESSIBILITY_JOB_INTERVAL_MILLIS, "0")
+
+        // enable service again and verify a notification
+        try {
+            mAccessibilityServiceRule.enableService()
+            runJobAndWaitUntilCompleted()
+            assertNotificationExist(permissionControllerPackage, ACCESSIBILITY_NOTIFICATION_ID)
+        } finally {
+            deleteDeviceConfigPrivacyProperty(ACCESSIBILITY_JOB_INTERVAL_MILLIS)
+        }
+    }
+
+    @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())
+        try {
+            val automation = getAutomation()
+            mAccessibilityServiceRule.enableService()
+            TestUtils.eventually(
+                {
+                    assertSafetyCenterIssueExist(
+                        SC_ACCESSIBILITY_SOURCE_ID,
+                        safetyCenterIssueId,
+                        SC_ACCESSIBILITY_ISSUE_TYPE_ID,
+                        automation)
+                },
+                TIMEOUT_MILLIS)
+            automation.destroy()
+        } finally {
+            setDeviceConfigPrivacyProperty(ACCESSIBILITY_LISTENER_ENABLED, false.toString())
+        }
+    }
+
+    @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)
+        TestUtils.awaitJobUntilRequestedState(
+            permissionControllerPackage,
+            ACCESSIBILITY_JOB_ID,
+            TIMEOUT_MILLIS,
+            getAutomation(),
+            "unknown"
+        )
+
+        runShellCommand(
+            "cmd jobscheduler reset-execution-quota -u " +
+                "${Process.myUserHandle().identifier} $permissionControllerPackage")
+        runShellCommand("cmd jobscheduler reset-schedule-quota")
+
+        // Setup up permission controller again (simulate a reboot)
+        val permissionControllerSetupIntent =
+            Intent(ACTION_SET_UP_ACCESSIBILITY_CHECK).apply {
+                setPackage(permissionControllerPackage)
+                setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+            }
+
+        // Query for the setup broadcast receiver
+        val resolveInfos =
+            context.packageManager.queryBroadcastReceivers(permissionControllerSetupIntent, 0)
+
+        if (resolveInfos.size > 0) {
+            context.sendBroadcast(permissionControllerSetupIntent)
+        } else {
+            context.sendBroadcast(
+                Intent().apply {
+                    setClassName(permissionControllerPackage, AccessibilityOnBootReceiver)
+                    setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+                    setPackage(permissionControllerPackage)
+                })
+        }
+
+        TestUtils.awaitJobUntilRequestedState(
+            permissionControllerPackage,
+            ACCESSIBILITY_JOB_ID,
+            TIMEOUT_MILLIS,
+            getAutomation(),
+            "waiting"
+        )
+    }
+
+    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_INTERVAL_MILLIS = "sc_accessibility_job_interval_millis"
+
+        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"
+        private const val ACTION_SET_UP_ACCESSIBILITY_CHECK =
+            "com.android.permissioncontroller.action.SET_UP_ACCESSIBILITY_CHECK"
+
+        @get:ClassRule
+        @JvmStatic
+        val ctsNotificationListenerHelper =
+            CtsNotificationListenerHelperRule(
+                InstrumentationRegistry.getInstrumentation().targetContext)
+    }
+}
diff --git a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt b/tests/tests/permission/src/android/permission/cts/AccessibilityTestService.kt
similarity index 60%
copy from tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
copy to tests/tests/permission/src/android/permission/cts/AccessibilityTestService.kt
index 101f200..9f5e3f1 100644
--- a/tests/tests/os/AutoRevokeQApp/src/android/os/cts/autorevokeqapp/MainActivity.kt
+++ b/tests/tests/permission/src/android/permission/cts/AccessibilityTestService.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,16 +14,11 @@
  * limitations under the License.
  */
 
-package android.os.cts.autorevokeqapp
+package android.permission.cts
 
-import android.app.Activity
-import android.os.Bundle
+import android.accessibility.cts.common.InstrumentedAccessibilityService
 
-class MainActivity : Activity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-
-        requestPermissions(arrayOf("android.permission.READ_CALENDAR"), 0)
-    }
-}
+/**
+ * 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..6880e22
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/BaseNotificationListenerCheckTest.java
@@ -0,0 +1,368 @@
+/*
+ * 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.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.compatibility.common.util.SystemUtil.waitForBroadcasts;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+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 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";
+
+    protected static final Long OVERRIDE_NOTIFICATION_LISTENER_CHECK_INTERVAL_MILLIS =
+            SECONDS.toMillis(0);
+
+    private static final String ACTION_SET_UP_NOTIFICATION_LISTENER_CHECK =
+            "com.android.permissioncontroller.action.SET_UP_NOTIFICATION_LISTENER_CHECK";
+    private static final String NotificationListenerOnBootReceiver =
+            "com.android.permissioncontroller.privacysources.SetupPeriodicNotificationListenerCheck";
+
+    /**
+     * 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));
+
+    @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);
+    }
+
+    /**
+     * Force a run of the notification listener check.
+     */
+    protected static void runNotificationListenerCheck() throws Throwable {
+        TestUtils.awaitJobUntilRequestedState(
+                PERMISSION_CONTROLLER_PKG,
+                NOTIFICATION_LISTENER_CHECK_JOB_ID,
+                UNEXPECTED_TIMEOUT_MILLIS,
+                sUiAutomation,
+                "waiting"
+        );
+
+        TestUtils.runJobAndWaitUntilCompleted(
+                PERMISSION_CONTROLLER_PKG,
+                NOTIFICATION_LISTENER_CHECK_JOB_ID,
+                UNEXPECTED_TIMEOUT_MILLIS,
+                sUiAutomation
+        );
+    }
+
+    /**
+     * 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);
+        runShellCommand("cmd jobscheduler reset-schedule-quota");
+    }
+
+    /**
+     * Reset the permission controllers state.
+     */
+    private static void resetPermissionController() throws Throwable {
+        clearAppState(PERMISSION_CONTROLLER_PKG);
+
+        // Wait until jobs are cleared
+        TestUtils.awaitJobUntilRequestedState(
+                PERMISSION_CONTROLLER_PKG,
+                NOTIFICATION_LISTENER_CHECK_JOB_ID,
+                UNEXPECTED_TIMEOUT_MILLIS,
+                sUiAutomation,
+                "unknown"
+        );
+
+        // Setup up permission controller again (simulate a reboot)
+        Intent permissionControllerSetupIntent = new Intent(
+                ACTION_SET_UP_NOTIFICATION_LISTENER_CHECK).setPackage(
+                PERMISSION_CONTROLLER_PKG).setFlags(FLAG_RECEIVER_FOREGROUND);
+
+        // Query for the setup broadcast receiver
+        List<ResolveInfo> resolveInfos = sContext.getPackageManager().queryBroadcastReceivers(
+                permissionControllerSetupIntent, 0);
+
+        if (resolveInfos.size() > 0) {
+            sContext.sendBroadcast(permissionControllerSetupIntent);
+        } else {
+            sContext.sendBroadcast(new Intent()
+                    .setClassName(PERMISSION_CONTROLLER_PKG, NotificationListenerOnBootReceiver)
+                    .setFlags(FLAG_RECEIVER_FOREGROUND)
+                    .setPackage(PERMISSION_CONTROLLER_PKG));
+        }
+        waitForBroadcasts();
+
+        // Wait until jobs are set up
+        TestUtils.awaitJobUntilRequestedState(
+                PERMISSION_CONTROLLER_PKG,
+                NOTIFICATION_LISTENER_CHECK_JOB_ID,
+                UNEXPECTED_TIMEOUT_MILLIS,
+                sUiAutomation,
+                "waiting"
+        );
+    }
+
+    /**
+     * 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 NotificationListenerUtils.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
+        NotificationListenerUtils.cancelNotifications(PERMISSION_CONTROLLER_PKG);
+    }
+}
diff --git a/tests/tests/permission/src/android/permission/cts/CorrectSensorPrivacyServiceTest.kt b/tests/tests/permission/src/android/permission/cts/CorrectSensorPrivacyServiceTest.kt
new file mode 100644
index 0000000..7de023c
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/CorrectSensorPrivacyServiceTest.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.hardware.SensorPrivacyManager
+import android.hardware.SensorPrivacyManager.Sensors.CAMERA
+import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE
+import android.hardware.SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE
+import android.os.Build
+import android.os.Process
+import android.platform.test.annotations.AppModeFull
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
+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.SystemUtil.waitForBroadcasts
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@AppModeFull(reason = "Cannot set system settings as instant app")
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+class CorrectSensorPrivacyServiceTest {
+
+    private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = instrumentation.targetContext
+    private val permissionControllerPackage = context.packageManager.permissionControllerPackageName
+    private val sensorPrivacyManager = context.getSystemService(SensorPrivacyManager::class.java)!!
+    private val unsupportedSensors: List<Int> = run {
+        val sensors = mutableListOf<Int>()
+        if (!isSensorSupported(CAMERA)) {
+            sensors.add(CAMERA)
+        }
+        if (!isSensorSupported(MICROPHONE)) {
+            sensors.add(MICROPHONE)
+        }
+        sensors
+    }
+    private var oldStates = mutableMapOf<Int, Boolean>()
+
+    @Before
+    fun setup() {
+        assumeTrue(unsupportedSensors.isNotEmpty())
+        runShellCommand("input keyevent KEYCODE_WAKEUP")
+        runShellCommand("wm dismiss-keyguard")
+        unsupportedSensors.forEach { sensor ->
+            oldStates[sensor] = isSensorPrivacyEnabled(sensor)
+        }
+        resetPermissionController()
+    }
+
+    @After
+    fun restoreState() {
+        unsupportedSensors.forEach { sensor ->
+            setSensorPrivacy(sensor, oldStates[sensor] ?: false)
+            oldStates.remove(sensor)
+        }
+    }
+
+    @Test
+    fun verifyJobCleansUpState() {
+        unsupportedSensors.forEach { sensor ->
+            setSensorPrivacy(sensor, true)
+        }
+
+        triggerJobScheduling()
+
+        eventually {
+            unsupportedSensors.forEach { sensor ->
+                assertFalse(isSensorPrivacyEnabled(sensor))
+            }
+        }
+    }
+
+    private fun isSensorSupported(sensor: Int): Boolean {
+        return callWithShellPermissionIdentity {
+            sensorPrivacyManager.supportsSensorToggle(sensor)
+        }
+    }
+
+    private fun isSensorPrivacyEnabled(sensor: Int): Boolean {
+        return callWithShellPermissionIdentity {
+            sensorPrivacyManager.isSensorPrivacyEnabled(TOGGLE_TYPE_SOFTWARE, sensor)
+        }
+    }
+
+    private fun resetPermissionController() {
+        PermissionUtils.clearAppState(permissionControllerPackage)
+        val automation = getAutomation()
+        TestUtils.awaitJobUntilRequestedState(
+            permissionControllerPackage,
+            CORRECT_STATE_JOB_ID,
+            TIMEOUT_MILLIS,
+            automation,
+            "unknown"
+        )
+        automation.destroy()
+
+        runShellCommand(
+            "cmd jobscheduler reset-execution-quota -u " +
+                "${Process.myUserHandle().identifier} $permissionControllerPackage")
+        runShellCommand("cmd jobscheduler reset-schedule-quota")
+    }
+
+    private fun setSensorPrivacy(sensor: Int, enable: Boolean) {
+        runWithShellPermissionIdentity {
+            sensorPrivacyManager.setSensorPrivacy(sensor, enable)
+        }
+    }
+
+    private fun triggerJobScheduling() {
+        val jobSetupReceiverIntent = Intent(ACTION_CORRECT_SENSOR_PRIVACY)
+        jobSetupReceiverIntent.setPackage(permissionControllerPackage)
+        jobSetupReceiverIntent.flags = Intent.FLAG_RECEIVER_FOREGROUND
+        val resolveInfos = context.packageManager.queryBroadcastReceivers(jobSetupReceiverIntent, 0)
+        assertTrue(
+            "No broadcast receivers found for $ACTION_CORRECT_SENSOR_PRIVACY", resolveInfos.size > 0
+        )
+
+        context.sendBroadcast(jobSetupReceiverIntent)
+        waitForBroadcasts()
+    }
+
+    private fun getAutomation(): UiAutomation {
+        return instrumentation.getUiAutomation(
+            UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES
+        )
+    }
+
+    companion object {
+        private const val CORRECT_STATE_JOB_ID = 10
+        private const val TIMEOUT_MILLIS: Long = 10000
+        private const val ACTION_CORRECT_SENSOR_PRIVACY =
+            "android.safetycenter.action.CORRECT_SENSOR_PRIVACY"
+        private const val SAFETY_CENTER_RECEIVER_CLASS =
+            "com.android.permissioncontroller.privacysources.SafetyCenterReceiver"
+    }
+}
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 e13f58b..8acf120 100644
--- a/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
+++ b/tests/tests/permission/src/android/permission/cts/LocationAccessCheckTest.java
@@ -18,6 +18,7 @@
 
 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.content.Context.BIND_AUTO_CREATE;
@@ -30,23 +31,19 @@
 
 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_JOB;
-import static com.android.server.job.nano.JobPackageHistoryProto.STOP_PERIODIC_JOB;
+import static com.android.compatibility.common.util.SystemUtil.waitForBroadcasts;
 
-import static org.junit.Assert.assertFalse;
-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.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
-import static java.lang.Math.max;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
+import android.app.PendingIntent;
 import android.app.UiAutomation;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -59,33 +56,28 @@
 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;
+import android.os.Process;
 import android.permission.cts.appthataccesseslocation.IAccessLocationOnCommand;
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.AsbSecurityTest;
 import android.platform.test.annotations.SystemUserOnly;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
-import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
 
 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.DeviceConfigStateChangerRule;
-import com.android.compatibility.common.util.DeviceConfigStateHelper;
-import com.android.compatibility.common.util.ProtoUtils;
 import com.android.compatibility.common.util.mainline.MainlineModule;
 import com.android.compatibility.common.util.mainline.ModuleDetector;
-import com.android.modules.utils.build.SdkLevel;
-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.After;
 import org.junit.AfterClass;
@@ -95,7 +87,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 
@@ -120,13 +111,16 @@
     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 long UNEXPECTED_TIMEOUT_MILLIS = 10000;
     private static final long EXPECTED_TIMEOUT_MILLIS = 15000;
@@ -147,6 +141,45 @@
             "com.android.permissioncontroller.permission.service"
                     + ".LocationAccessCheck$SetupPeriodicBackgroundLocationAccessCheck";
 
+
+    /**
+     * The result of {@link #assumeCanGetFineLocation()}, so we don't have to run it over and over
+     * again.
+     */
+    private static Boolean sCanAccessFineLocation = null;
+
+    private static ServiceConnection sConnection;
+    private static IAccessLocationOnCommand sLocationAccessor;
+
+    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 =
@@ -163,23 +196,42 @@
                     PROPERTY_LOCATION_ACCESS_CHECK_DELAY_MILLIS,
                     "50");
 
+    @BeforeClass
+    public static void beforeClassSetup() throws Exception {
+        reduceDelays();
+        allowNotificationAccess();
+        installBackgroundAccessApp();
+    }
+
     /**
-     * The result of {@link #assumeCanGetFineLocation()}, so we don't have to run it over and over
-     * again.
+     * Change settings so that permission controller can show location access notifications more
+     * often.
      */
-    private static Boolean sCanAccessFineLocation = null;
+    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);
+        });
+    }
 
-    private static ServiceConnection sConnection;
-    private static IAccessLocationOnCommand sLocationAccessor;
+    @AfterClass
+    public static void cleanupAfterClass() throws Throwable {
+        resetDelays();
+        uninstallBackgroundAccessApp();
+        disallowNotificationAccess();
+    }
 
-    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));
+    /**
+     * Reset settings so that permission controller runs normally.
+     */
+    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);
+        });
     }
 
     /**
@@ -275,14 +327,6 @@
     }
 
     /**
-     * Get the state of the job scheduler
-     */
-    public static JobSchedulerServiceDumpProto getJobSchedulerDump() throws Exception {
-        return ProtoUtils.getProto(sUiAutomation, JobSchedulerServiceDumpProto.class,
-                ProtoUtils.DUMPSYS_JOB_SCHEDULER);
-    }
-
-    /**
      * Clear all data of a package including permissions and files.
      *
      * @param pkg The name of the package to be cleared
@@ -292,86 +336,35 @@
         runShellCommand("pm clear --user -2 " + pkg);
     }
 
-    /**
-     * Get the last time the LOCATION_ACCESS_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 == LOCATION_ACCESS_CHECK_JOB_ID
-                    && historyEvent.event == event) {
-                lastTime = max(lastTime,
-                        System.currentTimeMillis() - historyEvent.timeSinceEventMs);
-            }
-        }
-
-        return lastTime;
+    private static boolean isJobReady() {
+        String jobStatus = runShellCommand("cmd jobscheduler get-job-state -u "
+                + Process.myUserHandle().getIdentifier() + " " + PERMISSION_CONTROLLER_PKG
+                + " " + LOCATION_ACCESS_CHECK_JOB_ID);
+        return jobStatus.contains("waiting");
     }
 
     /**
      * Force a run of the location check.
      */
     private static void runLocationCheck() throws Throwable {
-        // If the job isn't setup, do it before running a location check
-        if (!isLocationAccessJobSetup(myUserHandle().getIdentifier())) {
+        if (!isJobReady()) {
             setupLocationAccessCheckJob();
         }
 
-        // Sleep a little bit to make sure we don't have overlap in timing
-        Thread.sleep(1000);
+        TestUtils.awaitJobUntilRequestedState(
+                PERMISSION_CONTROLLER_PKG,
+                LOCATION_ACCESS_CHECK_JOB_ID,
+                EXPECTED_TIMEOUT_MILLIS,
+                sUiAutomation,
+                "waiting"
+        );
 
-        long beforeJob = System.currentTimeMillis();
-
-        // Sleep a little bit to avoid raciness in time keeping
-        Thread.sleep(1000);
-
-        runShellCommand(
-                "cmd jobscheduler run -u " + android.os.Process.myUserHandle().getIdentifier()
-                        + " -f " + PERMISSION_CONTROLLER_PKG + " 0");
-
-        eventually(() -> {
-            long startTime = getLastJobTime(START_PERIODIC_JOB);
-            assertTrue(startTime + " !> " + beforeJob, startTime > beforeJob);
-        }, EXPECTED_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;
-            if (SdkLevel.isAtLeastT()) {
-                stopTime = getLastJobTime(STOP_PERIODIC_JOB);
-            } else {
-                stopTime = getLastJobTime(STOP_JOB);
-            }
-            assertTrue(stopTime + " !> " + beforeJob, stopTime > beforeJob);
-        }, EXPECTED_TIMEOUT_MILLIS);
-    }
-
-    /**
-     * Get a notification thrown by the permission controller that is currently visible.
-     *
-     * @return The notification or {@code null} if there is none
-     */
-    private @Nullable StatusBarNotification getPermissionControllerNotification() throws Exception {
-        NotificationListenerService notificationService = NotificationListener.getInstance();
-
-        for (StatusBarNotification notification : notificationService.getActiveNotifications()) {
-            if (notification.getPackageName().equals(PERMISSION_CONTROLLER_PKG)) {
-                return notification;
-            }
-        }
-
-        return null;
+        TestUtils.runJobAndWaitUntilCompleted(
+                PERMISSION_CONTROLLER_PKG,
+                LOCATION_ACCESS_CHECK_JOB_ID,
+                EXPECTED_TIMEOUT_MILLIS,
+                sUiAutomation
+        );
     }
 
     /**
@@ -381,29 +374,8 @@
      * @return The notification or {@code null} if there is none
      */
     private StatusBarNotification getNotification(boolean cancelNotification) throws Throwable {
-        NotificationListenerService notificationService = NotificationListener.getInstance();
-
-        StatusBarNotification notification = getPermissionControllerNotification();
-        if (notification == null) {
-            return null;
-        }
-
-        if (notification.getId() == LOCATION_ACCESS_CHECK_NOTIFICATION_ID) {
-            if (cancelNotification) {
-                notificationService.cancelNotification(notification.getKey());
-
-                // Wait for notification to get canceled
-                eventually(() -> assertFalse(
-                        Arrays.asList(notificationService.getActiveNotifications()).contains(
-                                notification)), UNEXPECTED_TIMEOUT_MILLIS);
-            }
-
-            return notification;
-        }
-
-        Log.d(LOG_TAG, "Bad notification " + notification);
-
-        return null;
+        return NotificationListenerUtils.getNotificationForPackageAndId(PERMISSION_CONTROLLER_PKG,
+                LOCATION_ACCESS_CHECK_NOTIFICATION_ID, cancelNotification);
     }
 
     /**
@@ -418,32 +390,11 @@
     /**
      * Register {@link NotificationListener}.
      */
-    @BeforeClass
     public static void allowNotificationAccess() {
         runShellCommand("cmd notification allow_listener " + (new ComponentName(sContext,
                 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);
     }
@@ -459,7 +410,6 @@
         Thread.sleep(5000);
     }
 
-    @AfterClass
     public static void uninstallBackgroundAccessApp() {
         unbindService();
         runShellCommand("pm uninstall " + TEST_APP_PKG);
@@ -473,6 +423,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();
@@ -488,18 +453,15 @@
     /**
      * Skip each test for low ram device
      */
-    @Before
     public void assumeIsNotLowRamDevice() {
         assumeFalse(sActivityManager.isLowRamDevice());
     }
 
-    @Before
     public void wakeUpAndDismissKeyguard() {
         runShellCommand("input keyevent KEYCODE_WAKEUP");
         runShellCommand("wm dismiss-keyguard");
     }
 
-    @Before
     public void bindService() {
         sConnection = new ServiceConnection() {
             @Override
@@ -520,11 +482,20 @@
         sContext.bindService(testAppService, sConnection, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND);
     }
 
+    @Before
+    public void beforeEachTestSetup() throws Throwable {
+        assumeIsNotLowRamDevice();
+        wakeUpAndDismissKeyguard();
+        bindService();
+        resetPermissionControllerBeforeEachTest();
+        assumeCanGetFineLocation();
+    }
+
     /**
      * Reset the permission controllers state before each test
      */
-    @Before
     public void resetPermissionControllerBeforeEachTest() throws Throwable {
+        //setupLocationAccessCheckJob();
         // Has to be before resetPermissionController to make sure enablement time is the reset time
         // of permission controller
         enableLocationAccessCheck();
@@ -537,14 +508,15 @@
         runShellCommand(
                 "cmd jobscheduler reset-execution-quota -u " + myUserHandle().getIdentifier() + " "
                         + PERMISSION_CONTROLLER_PKG);
+        runShellCommand("cmd jobscheduler reset-schedule-quota");
     }
 
     /**
      * 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();
     }
@@ -553,8 +525,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();
     }
@@ -562,7 +534,6 @@
     /**
      * Make sure fine location can be accessed at all.
      */
-    @Before
     public void assumeCanGetFineLocation() {
         if (sCanAccessFineLocation == null) {
             Criteria crit = new Criteria();
@@ -605,38 +576,22 @@
      */
     private static void resetPermissionController() throws Throwable {
         clearPackageData(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);
+        TestUtils.awaitJobUntilRequestedState(
+                PERMISSION_CONTROLLER_PKG,
+                LOCATION_ACCESS_CHECK_JOB_ID,
+                UNEXPECTED_TIMEOUT_MILLIS,
+                sUiAutomation,
+                "unknown"
+        );
 
         setupLocationAccessCheckJob();
-
-        // Wait until jobs are set up
-        eventually(() -> {
-            assertTrue("LocationAccessCheck job not found",
-                    isLocationAccessJobSetup(currentUserId));
-        }, UNEXPECTED_TIMEOUT_MILLIS);
-    }
-
-    private static boolean isLocationAccessJobSetup(int currentUserId) throws Exception {
-        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("LocationAccessCheck")) {
-                return true;
-            }
-        }
-        return false;
+        TestUtils.awaitJobUntilRequestedState(
+                PERMISSION_CONTROLLER_PKG,
+                LOCATION_ACCESS_CHECK_JOB_ID,
+                UNEXPECTED_TIMEOUT_MILLIS,
+                sUiAutomation,
+                "waiting"
+        );
     }
 
     private static void setupLocationAccessCheckJob() {
@@ -657,44 +612,31 @@
                     .setFlags(FLAG_RECEIVER_FOREGROUND)
                     .setPackage(PERMISSION_CONTROLLER_PKG));
         }
+        waitForBroadcasts();
     }
 
     /**
      * Unregister {@link NotificationListener}.
      */
-    @AfterClass
     public static void disallowNotificationAccess() {
         runShellCommand("cmd notification disallow_listener " + (new ComponentName(sContext,
                 NotificationListener.class)).flattenToString());
     }
 
-    /**
-     * 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();
-        });
+    @After
+    public void cleanupAfterEachTest() throws Throwable {
+        resetPrivacyConfig();
+        locationUnbind();
     }
 
     /**
      * 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();
     }
 
-    @After
     public void locationUnbind() throws Throwable {
         unbindService();
     }
@@ -703,7 +645,6 @@
     public void notificationIsShown() throws Throwable {
         accessLocation();
         runLocationCheck();
-
         eventually(() -> assertNotNull(getNotification(true)), EXPECTED_TIMEOUT_MILLIS);
     }
 
@@ -737,6 +678,7 @@
 
         // Wait until package is cleared and permission controller has cleared the state
         Thread.sleep(10000);
+        waitForBroadcasts();
 
         // Clearing removed the permissions, hence grant them again
         grantPermissionToTestApp(ACCESS_FINE_LOCATION);
@@ -762,7 +704,7 @@
         Thread.sleep(2000);
 
         installBackgroundAccessApp();
-
+        waitForBroadcasts();
         accessLocation();
         runLocationCheck();
 
@@ -780,12 +722,14 @@
         eventually(() -> assertNotNull(getNotification(false)), EXPECTED_TIMEOUT_MILLIS);
 
         uninstallBackgroundAccessApp();
+        // wait for permission controller (broadcast receiver) to clean up things
+        Thread.sleep(5000);
+        waitForBroadcasts();
 
         try {
             eventually(() -> assertNull(getNotification(false)), UNEXPECTED_TIMEOUT_MILLIS);
         } finally {
             installBackgroundAccessApp();
-            getNotification(true);
         }
     }
 
@@ -838,6 +782,7 @@
         assertNull(getNotification(false));
 
         enableLocationAccessCheck();
+        Thread.sleep(2000);
 
         // Trigger update of location enable time. In the real world it enabling happens on the
         // first location check. I.e. accesses before this location check are ignored.
@@ -882,4 +827,24 @@
         runLocationCheck();
         assertNull(getNotification(false));
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+    public void notificationOnClickOpensSafetyCenter() throws Throwable {
+        assumeTrue(SafetyCenterUtils.deviceSupportsSafetyCenter(sContext));
+        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..8999a93
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationListenerCheckTest.java
@@ -0,0 +1,211 @@
+/*
+ * 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 notificationIsShownAgainAfterDisableAndReenableAppNotificationListener()
+            throws Throwable {
+        runNotificationListenerCheck();
+
+        eventually(() -> assertNotNull(getNotification(true)), UNEXPECTED_TIMEOUT_MILLIS);
+
+        // Disallow NLS, and run NLS check job. This  should clear NLS off notified list
+        disallowTestAppNotificationListenerService();
+        runNotificationListenerCheck();
+
+        // Re-allow NLS, and run NLS check job. This work now that it's cleared NLS off notified
+        // list
+        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..dffefe7
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/NotificationListenerUtils.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.permission.cts.TestUtils.ensure
+import android.permission.cts.TestUtils.eventually
+import android.service.notification.StatusBarNotification
+import org.junit.Assert
+
+object NotificationListenerUtils {
+
+    private const val NOTIFICATION_CANCELLATION_TIMEOUT_MILLIS = 5000L
+    private const val NOTIFICATION_WAIT_MILLIS = 2000L
+
+    @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 notificationService = NotificationListener.getInstance()
+        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 notificationService = NotificationListener.getInstance()
+        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()
+        val notificationService = NotificationListener.getInstance()
+        for (notification in notificationService.activeNotifications) {
+            if (notification.packageName == packageName) {
+                notifications.add(notification)
+            }
+        }
+        return notifications
+    }
+
+    /**
+     * 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 notifications: List<StatusBarNotification> = getNotifications(pkg)
+        if (notifications.isEmpty()) {
+            return null
+        }
+        for (notification in notifications) {
+            if (notification.id == id) {
+                if (cancelNotification) {
+                    cancelNotification(pkg, id)
+                }
+                return notification
+            }
+        }
+        return null
+    }
+}
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..7514079
--- /dev/null
+++ b/tests/tests/permission/src/android/permission/cts/SafetyCenterUtils.kt
@@ -0,0 +1,151 @@
+/*
+ * 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
+    fun deleteDeviceConfigPrivacyProperty(
+        propertyName: String,
+        uiAutomation: UiAutomation = instrumentation.uiAutomation
+    ) {
+        runWithShellPermissionIdentity(uiAutomation) {
+            DeviceConfig.deleteProperty(DeviceConfig.NAMESPACE_PRIVACY, propertyName)
+        }
+    }
+
+    @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/permission2/res/raw/android_manifest.xml b/tests/tests/permission2/res/raw/android_manifest.xml
index 9fa4472..dc7238a 100644
--- a/tests/tests/permission2/res/raw/android_manifest.xml
+++ b/tests/tests/permission2/res/raw/android_manifest.xml
@@ -1114,6 +1114,18 @@
                 android:description="@string/permdesc_readMediaImage"
                 android:protectionLevel="dangerous" />
 
+    <!-- Allows an application to read image or video files from external storage that a user has
+      selected via the permission prompt photo picker. Apps can check this permission to verify that
+      a user has decided to use the photo picker, instead of granting access to
+      {@link #READ_MEDIA_IMAGES or #READ_MEDIA_VIDEO}. It does not prevent apps from accessing the
+      standard photo picker manually.
+   <p>Protection level: dangerous -->
+    <permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"
+        android:permissionGroup="android.permission-group.UNDEFINED"
+        android:label="@string/permlab_readVisualUserSelect"
+        android:description="@string/permdesc_readVisualUserSelect"
+        android:protectionLevel="dangerous" />
+
     <!-- Allows an application to write to external storage.
          <p class="note"><strong>Note:</strong> If <em>both</em> your <a
          href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#min">{@code
diff --git a/tests/tests/permission2/src/android/permission2/cts/RuntimePermissionProperties.kt b/tests/tests/permission2/src/android/permission2/cts/RuntimePermissionProperties.kt
index a1eca49..e10131c 100644
--- a/tests/tests/permission2/src/android/permission2/cts/RuntimePermissionProperties.kt
+++ b/tests/tests/permission2/src/android/permission2/cts/RuntimePermissionProperties.kt
@@ -38,6 +38,7 @@
 import android.Manifest.permission.READ_CELL_BROADCASTS
 import android.Manifest.permission.READ_CONTACTS
 import android.Manifest.permission.READ_EXTERNAL_STORAGE
+import android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
 import android.Manifest.permission.READ_PHONE_NUMBERS
 import android.Manifest.permission.READ_PHONE_STATE
 import android.Manifest.permission.READ_SMS
@@ -161,6 +162,9 @@
         expectedPerms.add(POST_NOTIFICATIONS)
         expectedPerms.add(NEARBY_WIFI_DEVICES)
 
+        // Add runtime permissions added in U which were _not_ split from a previously existing
+        // runtime permission
+        expectedPerms.add(READ_MEDIA_VISUAL_USER_SELECTED)
         assertThat(expectedPerms).containsExactlyElementsIn(platformRuntimePerms.map { it.name })
     }
 }
diff --git a/tests/tests/permission3/src/android/permission3/cts/BasePermissionTest.kt b/tests/tests/permission3/src/android/permission3/cts/BasePermissionTest.kt
index fe02deb..4e6f031 100644
--- a/tests/tests/permission3/src/android/permission3/cts/BasePermissionTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/BasePermissionTest.kt
@@ -49,6 +49,7 @@
     companion object {
         const val APK_DIRECTORY = "/data/local/tmp/cts/permission3"
 
+        const val QUICK_CHECK_TIMEOUT_MILLIS = 100L
         const val IDLE_TIMEOUT_MILLIS: Long = 1000
         const val UNEXPECTED_TIMEOUT_MILLIS = 1000
         const val TIMEOUT_MILLIS: Long = 20000
diff --git a/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt b/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt
index b504e80..9cd70c0 100644
--- a/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/BaseUsePermissionTest.kt
@@ -32,9 +32,11 @@
 import android.text.Spanned
 import android.text.style.ClickableSpan
 import android.view.View
+import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
 import com.android.compatibility.common.util.SystemUtil.eventually
 import com.android.modules.utils.build.SdkLevel
 import org.junit.After
+import org.junit.Assert
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
@@ -113,6 +115,9 @@
 
         const val REQUEST_LOCATION_MESSAGE = "permgrouprequest_location"
 
+        // The highest SDK for which the system will show a "low SDK" warning when launching the app
+        const val MAX_SDK_FOR_SDK_WARNING = 27
+
         val STORAGE_AND_MEDIA_PERMISSIONS = setOf(
             android.Manifest.permission.READ_EXTERNAL_STORAGE,
             android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
@@ -209,8 +214,10 @@
         uninstallPackage(APP_PACKAGE_NAME, requireSuccess = false)
     }
 
-    protected fun clearTargetSdkWarning() =
-        click(By.res("android:id/button1"))
+    protected fun clearTargetSdkWarning(timeoutMillis: Long = TIMEOUT_MILLIS) =
+        waitFindObjectOrNull(By.res("android:id/button1"), timeoutMillis)?.click()?.also {
+            waitForIdle()
+        }
 
     protected fun clickPermissionReviewContinue() {
         if (isAutomotive || isWatch) {
@@ -231,6 +238,8 @@
     protected fun approvePermissionReview() {
         startAppActivityAndAssertResultCode(Activity.RESULT_OK) {
             clickPermissionReviewContinue()
+            waitForIdle()
+            clearTargetSdkWarning()
         }
     }
 
@@ -294,6 +303,13 @@
             }
         )
         waitForIdle()
+
+        // Clear the low target SDK warning message if it's expected
+        if (getTargetSdk() <= MAX_SDK_FOR_SDK_WARNING) {
+            clearTargetSdkWarning(timeoutMillis = QUICK_CHECK_TIMEOUT_MILLIS)
+            waitForIdle()
+        }
+
         // Notification permission prompt is shown first, so get it out of the way
         clickNotificationPermissionRequestAllowButtonIfAvailable()
         // Perform the post-request action
@@ -471,26 +487,39 @@
         }
     }
 
-    protected fun grantAppPermissions(vararg permissions: String, targetSdk: Int = 30) {
-        setAppPermissionState(*permissions, state = PermissionState.ALLOWED, isLegacyApp = false,
-                targetSdk = targetSdk)
+    protected fun grantAppPermissions(vararg permissions: String) {
+        setAppPermissionState(*permissions, state = PermissionState.ALLOWED, isLegacyApp = false)
     }
 
     protected fun revokeAppPermissions(
         vararg permissions: String,
-        isLegacyApp: Boolean = false,
-        targetSdk: Int = 30
+        isLegacyApp: Boolean = false
     ) {
         setAppPermissionState(*permissions, state = PermissionState.DENIED,
-                isLegacyApp = isLegacyApp, targetSdk = targetSdk)
+                isLegacyApp = isLegacyApp)
+    }
+
+    protected fun getTargetSdk(packageName: String = APP_PACKAGE_NAME): Int {
+        return callWithShellPermissionIdentity {
+            try {
+                context.packageManager.getApplicationInfo(packageName, 0).targetSdkVersion
+            } catch (e: PackageManager.NameNotFoundException) {
+                Assert.fail("Package $packageName not found")
+                -1
+            }
+        }
     }
 
     private fun setAppPermissionState(
         vararg permissions: String,
         state: PermissionState,
-        isLegacyApp: Boolean,
-        targetSdk: Int
+        isLegacyApp: Boolean
     ) {
+        val targetSdk = getTargetSdk()
+        if (targetSdk <= MAX_SDK_FOR_SDK_WARNING) {
+            clearTargetSdkWarning(QUICK_CHECK_TIMEOUT_MILLIS)
+        }
+
         if (isTv) {
             // Dismiss DeprecatedTargetSdkVersionDialog, if present
             if (waitFindObjectOrNull(By.text(APP_PACKAGE_NAME), 1000L) != null) {
@@ -580,7 +609,9 @@
                                 By.text(getPermissionControllerString(
                                         ALLOW_FOREGROUND_PREFERENCE_TEXT))
                             } else {
-                                byAnyText(getPermissionControllerResString(ALLOW_BUTTON_TEXT),getPermissionControllerResString(ALLOW_ALL_FILES_BUTTON_TEXT))
+                                byAnyText(getPermissionControllerResString(ALLOW_BUTTON_TEXT),
+                                        getPermissionControllerResString(
+                                                ALLOW_ALL_FILES_BUTTON_TEXT))
                             }
                         PermissionState.DENIED ->
                             if (!isLegacyApp && hasAskButton(permission)) {
diff --git a/tests/tests/permission3/src/android/permission3/cts/MediaPermissionTest.kt b/tests/tests/permission3/src/android/permission3/cts/MediaPermissionTest.kt
index 8bd15373..df2810e 100644
--- a/tests/tests/permission3/src/android/permission3/cts/MediaPermissionTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/MediaPermissionTest.kt
@@ -39,9 +39,7 @@
     @Test
     fun testWhenRESIsGrantedFromGrantDialogThenShouldGrantAllPermissions() {
         installPackage(APP_APK_PATH_23)
-        requestAppPermissionsAndAssertResult(
-            android.Manifest.permission.READ_EXTERNAL_STORAGE to true
-        ) {
+        requestAppPermissionsAndAssertResult(Manifest.permission.READ_EXTERNAL_STORAGE to true) {
             clickPermissionRequestAllowButton()
         }
         assertStorageAndMediaPermissionState(true)
@@ -50,30 +48,28 @@
     @Test
     fun testWhenRESIsGrantedManuallyThenShouldGrantAllPermissions() {
         installPackage(APP_APK_PATH_23)
-        grantAppPermissions(android.Manifest.permission.READ_EXTERNAL_STORAGE, targetSdk = 23)
+        grantAppPermissions(Manifest.permission.READ_EXTERNAL_STORAGE)
         assertStorageAndMediaPermissionState(true)
     }
 
     @Test
     fun testWhenAuralIsGrantedManuallyThenShouldGrantAllPermissions() {
         installPackage(APP_APK_PATH_23)
-        grantAppPermissions(android.Manifest.permission.READ_MEDIA_AUDIO, targetSdk = 23)
+        grantAppPermissions(Manifest.permission.READ_MEDIA_AUDIO)
         assertStorageAndMediaPermissionState(true)
     }
 
     @Test
     fun testWhenVisualIsGrantedManuallyThenShouldGrantAllPermissions() {
         installPackage(APP_APK_PATH_23)
-        grantAppPermissions(android.Manifest.permission.READ_MEDIA_VIDEO, targetSdk = 23)
+        grantAppPermissions(Manifest.permission.READ_MEDIA_VIDEO)
         assertStorageAndMediaPermissionState(true)
     }
 
     @Test
     fun testWhenRESIsDeniedFromGrantDialogThenShouldDenyAllPermissions() {
         installPackage(APP_APK_PATH_23)
-        requestAppPermissionsAndAssertResult(
-            android.Manifest.permission.READ_EXTERNAL_STORAGE to false
-        ) {
+        requestAppPermissionsAndAssertResult(Manifest.permission.READ_EXTERNAL_STORAGE to false) {
             clickPermissionRequestDenyButton()
         }
         assertStorageAndMediaPermissionState(false)
@@ -82,16 +78,16 @@
     @Test
     fun testWhenRESIsDeniedManuallyThenShouldDenyAllPermissions() {
         installPackage(APP_APK_PATH_23)
-        grantAppPermissions(android.Manifest.permission.READ_EXTERNAL_STORAGE, targetSdk = 23)
-        revokeAppPermissions(android.Manifest.permission.READ_EXTERNAL_STORAGE, targetSdk = 23)
+        grantAppPermissions(Manifest.permission.READ_EXTERNAL_STORAGE)
+        revokeAppPermissions(Manifest.permission.READ_EXTERNAL_STORAGE)
         assertStorageAndMediaPermissionState(false)
     }
 
     @Test
     fun testWhenAuralIsDeniedManuallyThenShouldDenyAllPermissions() {
         installPackage(APP_APK_PATH_23)
-        grantAppPermissions(android.Manifest.permission.READ_MEDIA_AUDIO, targetSdk = 23)
-        revokeAppPermissions(android.Manifest.permission.READ_MEDIA_AUDIO, targetSdk = 23)
+        grantAppPermissions(Manifest.permission.READ_MEDIA_AUDIO)
+        revokeAppPermissions(Manifest.permission.READ_MEDIA_AUDIO)
         assertStorageAndMediaPermissionState(false)
     }
 
@@ -100,8 +96,8 @@
         // TODO: Re-enable after b/239249703 is fixed
         Assume.assumeFalse("skip on TV due to flaky", isTv)
         installPackage(APP_APK_PATH_23)
-        grantAppPermissions(android.Manifest.permission.READ_MEDIA_VIDEO, targetSdk = 23)
-        revokeAppPermissions(android.Manifest.permission.READ_MEDIA_VIDEO, targetSdk = 23)
+        grantAppPermissions(Manifest.permission.READ_MEDIA_VIDEO)
+        revokeAppPermissions(Manifest.permission.READ_MEDIA_VIDEO)
         assertStorageAndMediaPermissionState(false)
     }
 
@@ -109,8 +105,8 @@
     fun testWhenA33AppRequestsStorageThenNoDialogAndNoGrant() {
         installPackage(APP_APK_PATH_MEDIA_PERMISSION_33_WITH_STORAGE)
         requestAppPermissions(
-            android.Manifest.permission.READ_EXTERNAL_STORAGE,
-            android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+            Manifest.permission.READ_EXTERNAL_STORAGE,
+            Manifest.permission.WRITE_EXTERNAL_STORAGE
         ) {
         }
         assertStorageAndMediaPermissionState(false)
@@ -119,30 +115,30 @@
     @Test
     fun testWhenA33AppRequestsAuralThenDialogAndGrant() {
         installPackage(APP_APK_PATH_LATEST)
-        requestAppPermissions(android.Manifest.permission.READ_MEDIA_AUDIO) {
+        requestAppPermissions(Manifest.permission.READ_MEDIA_AUDIO) {
             clickPermissionRequestAllowButton()
         }
-        assertAppHasPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE, false)
-        assertAppHasPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, false)
-        assertAppHasPermission(android.Manifest.permission.READ_MEDIA_AUDIO, true)
-        assertAppHasPermission(android.Manifest.permission.READ_MEDIA_VIDEO, false)
-        assertAppHasPermission(android.Manifest.permission.READ_MEDIA_IMAGES, false)
+        assertAppHasPermission(Manifest.permission.READ_EXTERNAL_STORAGE, false)
+        assertAppHasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, false)
+        assertAppHasPermission(Manifest.permission.READ_MEDIA_AUDIO, true)
+        assertAppHasPermission(Manifest.permission.READ_MEDIA_VIDEO, false)
+        assertAppHasPermission(Manifest.permission.READ_MEDIA_IMAGES, false)
     }
 
     @Test
     fun testWhenA33AppRequestsVisualThenDialogAndGrant() {
         installPackage(APP_APK_PATH_LATEST)
         requestAppPermissions(
-            android.Manifest.permission.READ_MEDIA_VIDEO,
-            android.Manifest.permission.READ_MEDIA_IMAGES
+            Manifest.permission.READ_MEDIA_VIDEO,
+            Manifest.permission.READ_MEDIA_IMAGES
         ) {
             clickPermissionRequestAllowButton()
         }
-        assertAppHasPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE, false)
-        assertAppHasPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, false)
-        assertAppHasPermission(android.Manifest.permission.READ_MEDIA_AUDIO, false)
-        assertAppHasPermission(android.Manifest.permission.READ_MEDIA_VIDEO, true)
-        assertAppHasPermission(android.Manifest.permission.READ_MEDIA_IMAGES, true)
+        assertAppHasPermission(Manifest.permission.READ_EXTERNAL_STORAGE, false)
+        assertAppHasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, false)
+        assertAppHasPermission(Manifest.permission.READ_MEDIA_AUDIO, false)
+        assertAppHasPermission(Manifest.permission.READ_MEDIA_VIDEO, true)
+        assertAppHasPermission(Manifest.permission.READ_MEDIA_IMAGES, true)
     }
 
     @Test
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionAttributionTest.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionAttributionTest.kt
index 96498b3..b7f1057 100644
--- a/tests/tests/permission3/src/android/permission3/cts/PermissionAttributionTest.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionAttributionTest.kt
@@ -22,13 +22,11 @@
 import android.content.Intent
 import android.location.LocationManager
 import android.os.Build
-import android.provider.DeviceConfig
 import android.support.test.uiautomator.By
 import androidx.test.filters.SdkSuppress
 import com.android.compatibility.common.util.AppOpsUtils.setOpMode
 import com.android.compatibility.common.util.CtsDownstreamingTest
 import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
-import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
 import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
@@ -48,7 +46,6 @@
     private val micLabel = packageManager.getPermissionGroupInfo(
         android.Manifest.permission_group.MICROPHONE, 0).loadLabel(packageManager).toString()
     val locationManager = context.getSystemService(LocationManager::class.java)!!
-    private var wasEnabled = false
 
     @Before
     fun installAppLocationProviderAndAllowMockLocation() {
@@ -64,15 +61,11 @@
         setOpMode(
             context.packageName, AppOpsManager.OPSTR_MOCK_LOCATION, AppOpsManager.MODE_ALLOWED
         )
-        wasEnabled = setSubattributionEnabledStateIfNeeded(true)
     }
 
     @After
     fun teardown() {
         locationManager.removeTestProvider(APP_PACKAGE_NAME)
-        if (!wasEnabled) {
-            setSubattributionEnabledStateIfNeeded(false)
-        }
     }
 
     @Test
@@ -119,24 +112,10 @@
         assertEquals(Activity.RESULT_OK, result.resultCode)
     }
 
-    private fun setSubattributionEnabledStateIfNeeded(shouldBeEnabled: Boolean): Boolean {
-        var currentlyEnabled = false
-        runWithShellPermissionIdentity {
-            currentlyEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
-                FLAG_SUBATTRIBUTION, false)
-            if (currentlyEnabled != shouldBeEnabled) {
-                DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY, FLAG_SUBATTRIBUTION,
-                    shouldBeEnabled.toString(), false)
-            }
-        }
-        return currentlyEnabled
-    }
-
     companion object {
         const val APP_APK_PATH = "$APK_DIRECTORY/CtsAccessMicrophoneAppLocationProvider.apk"
         const val APP_PACKAGE_NAME = "android.permission3.cts.accessmicrophoneapplocationprovider"
         const val APP_LABEL = "LocationProviderWithMicApp"
         const val ATTRIBUTION_LABEL = "Attribution Label"
-        const val FLAG_SUBATTRIBUTION = "permissions_hub_subattribution_enabled"
     }
 }
diff --git a/tests/tests/permission3/src/android/permission3/cts/PermissionTest23.kt b/tests/tests/permission3/src/android/permission3/cts/PermissionTest23.kt
index f9c4496..c9cc81a 100644
--- a/tests/tests/permission3/src/android/permission3/cts/PermissionTest23.kt
+++ b/tests/tests/permission3/src/android/permission3/cts/PermissionTest23.kt
@@ -247,7 +247,7 @@
             android.Manifest.permission.CALL_PHONE,
             android.Manifest.permission.RECORD_AUDIO,
             android.Manifest.permission.CAMERA,
-            android.Manifest.permission.READ_EXTERNAL_STORAGE, targetSdk = 23
+            android.Manifest.permission.READ_EXTERNAL_STORAGE
         )
         // Don't use UI for granting location and sensor permissions as they show another dialog
         uiAutomation.grantRuntimePermission(
@@ -307,12 +307,12 @@
         Assume.assumeFalse("other form factors might not support the ask button",
                 isTv || isAutomotive || isWatch)
 
-        grantAppPermissions(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION, targetSdk = 23)
+        grantAppPermissions(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION)
         assertAppHasPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION, true)
         assertAppHasPermission(android.Manifest.permission.ACCESS_FINE_LOCATION, true)
         assertAppHasPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION, true)
 
-        revokeAppPermissions(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION, targetSdk = 23)
+        revokeAppPermissions(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION)
         SystemUtil.runWithShellPermissionIdentity {
             val perms = listOf(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION,
                     android.Manifest.permission.ACCESS_FINE_LOCATION,
diff --git a/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt b/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
index b224d3c..f0ac8fd 100644
--- a/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
+++ b/tests/tests/permission4/src/android/permission4/cts/CameraMicIndicatorsPermissionTest.kt
@@ -26,14 +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.platform.test.annotations.AsbSecurityTest
 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.UiSelector
+import androidx.annotation.RequiresApi
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.compatibility.common.util.DisableAnimationRule
@@ -41,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 com.android.sts.common.util.StsExtraBusinessLogicTestCase
 import org.junit.After
 import org.junit.Assert
@@ -52,7 +59,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
 
@@ -75,6 +81,8 @@
 private const val UNEXPECTED_TIMEOUT_MILLIS = 1000
 private const val TIMEOUT_MILLIS: Long = 20000
 private const val TV_MIC_INDICATOR_WINDOW_TITLE = "MicrophoneCaptureIndicator"
+private const val MIC_LABEL_NAME = "microphone_toggle_label_qs"
+private const val CAMERA_LABEL_NAME = "camera_toggle_label_qs"
 
 class CameraMicIndicatorsPermissionTest : StsExtraBusinessLogicTestCase {
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -87,11 +95,13 @@
 
     private val isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
     private val isCar = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
-    private var wasEnabled = false
-    private val micLabel = packageManager.getPermissionGroupInfo(
-        Manifest.permission_group.MICROPHONE, 0).loadLabel(packageManager).toString().toLowerCase()
+    private val safetyCenterMicLabel = getPermissionControllerString(MIC_LABEL_NAME)
+    private val safetyCenterCameraLabel = getPermissionControllerString(CAMERA_LABEL_NAME)
     private val cameraLabel = packageManager.getPermissionGroupInfo(
         Manifest.permission_group.CAMERA, 0).loadLabel(packageManager).toString().toLowerCase()
+    private val micLabel = packageManager.getPermissionGroupInfo(
+        Manifest.permission_group.MICROPHONE, 0).loadLabel(packageManager).toString().toLowerCase()
+    private var wasEnabled = false
     private var isScreenOn = false
     private var screenTimeoutBeforeTest: Long = 0L
 
@@ -107,7 +117,7 @@
 
     private val safetyCenterEnabled = callWithShellPermissionIdentity {
         DeviceConfig.getString(DeviceConfig.NAMESPACE_PRIVACY,
-                SAFETY_CENTER_ENABLED, false.toString())
+            SAFETY_CENTER_ENABLED, false.toString())
     }
 
     @Before
@@ -119,8 +129,6 @@
             Settings.System.putLong(
                 context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT, 1800000L
             )
-            DeviceConfig.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
-                    SAFETY_CENTER_ENABLED, false.toString(), false)
         }
 
         if (!isScreenOn) {
@@ -199,8 +207,6 @@
 
     @Test
     fun testMicIndicator() {
-        // skip test for automototive devices, as Mic indicator is not a MUST requirement as per CDD
-        assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE))
         changeSafetyCenterFlag(false.toString())
         testCameraAndMicIndicator(useMic = true, useCamera = false)
     }
@@ -233,9 +239,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)
@@ -243,28 +247,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,
@@ -273,14 +276,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,
@@ -454,14 +456,28 @@
                 return@eventually
             }
             if (useMic) {
-                val iconView = uiDevice.findObject(By.descContains(micLabel))
-                assertNotNull("View with description $micLabel not found", iconView)
+                var micIdentifier: String
+                var iconView = if (safetyCenterEnabled) {
+                    micIdentifier = safetyCenterMicLabel
+                    waitFindObject(By.text(micIdentifier))
+                } else {
+                    micIdentifier = micLabel
+                    waitFindObject(By.descContains(micIdentifier))
+                }
+                assertNotNull("View with text/description $micIdentifier not found", iconView)
             }
             if (useCamera) {
-                val iconView = uiDevice.findObject(By.descContains(cameraLabel))
-                assertNotNull("View with text $APP_LABEL not found", iconView)
+                var camIdentifier: String
+                var iconView = if (safetyCenterEnabled) {
+                    camIdentifier = safetyCenterCameraLabel
+                    waitFindObject(By.text(camIdentifier))
+                } else {
+                    camIdentifier = cameraLabel
+                    waitFindObject(By.descContains(camIdentifier))
+                }
+                assertNotNull("View with text/description $camIdentifier not found", iconView)
             }
-            val appView = uiDevice.findObject(By.textContains(APP_LABEL))
+            var appView = waitFindObject(By.textContains(APP_LABEL))
             assertNotNull("View with text $APP_LABEL not found", appView)
             if (safetyCenterEnabled) {
                 assertTrue("Did not find safety center views",
@@ -495,16 +511,21 @@
             "Did not find shell package"
         }
 
-        val usageViews = if (safetyCenterEnabled) {
-            uiDevice.findObjects(By.res(SAFETY_CENTER_ITEM_ID))
+        if (safetyCenterEnabled) {
+            var micView = waitFindObject(By.text(safetyCenterMicLabel))
+            assertNotNull("View with text $micLabel not found", micView)
+            var camView = waitFindObject(By.text(safetyCenterCameraLabel))
+            assertNotNull("View with text $cameraLabel not found", camView)
+            var shellView = waitFindObject(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(clickChip: Boolean): Boolean {
@@ -540,7 +561,52 @@
     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 waitFindObject(selector: BySelector): UiObject2? {
+        waitForIdle()
+        return findObjectWithRetry({ t -> UiAutomatorUtils.waitFindObject(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)
+        }
+    }
+
+    private fun getPermissionControllerString(resourceName: String): String {
+        val permissionControllerPkg = context.packageManager.permissionControllerPackageName
+        try {
+            val permissionControllerContext =
+                context.createPackageContext(permissionControllerPkg, 0)
+            val resourceId =
+                permissionControllerContext.resources.getIdentifier(
+                    resourceName, "string", "com.android.permissioncontroller")
+            return permissionControllerContext.getString(resourceId)
+        } catch (e: PackageManager.NameNotFoundException) {
+            throw RuntimeException(e)
         }
     }
 }
diff --git a/tests/tests/provider/OWNERS b/tests/tests/provider/OWNERS
index 1e318ea..7c9a6b2 100644
--- a/tests/tests/provider/OWNERS
+++ b/tests/tests/provider/OWNERS
@@ -1,7 +1,13 @@
-# Bug component: 655625
-
-include platform/frameworks/base:/core/java/android/os/storage/OWNERS
-
 tgunn@google.com
 nicksauer@google.com
 nona@google.com
+
+# Storage team ownership
+
+# Bug component: 655625 = per-file *MediaStore*
+
+per-file *MediaStore* = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
+per-file Android.bp = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
+per-file AndroidManifest.xml = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
+per-file AndroidTest.xml = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
+per-file OWNERS = file:platform/frameworks/base:/core/java/android/os/storage/OWNERS
diff --git a/tests/tests/provider/app/GalleryTestApp/Android.bp b/tests/tests/provider/app/GalleryTestApp/Android.bp
index 2cf157e..dfda56d 100644
--- a/tests/tests/provider/app/GalleryTestApp/Android.bp
+++ b/tests/tests/provider/app/GalleryTestApp/Android.bp
@@ -29,5 +29,6 @@
     optimize: {
         enabled: false,
     },
-    min_sdk_version : "29",
+    min_sdk_version: "29",
+    updatable: true,
 }
diff --git a/tests/uwb/Android.bp b/tests/tests/sdksandbox/webkit/Android.bp
similarity index 63%
rename from tests/uwb/Android.bp
rename to tests/tests/sdksandbox/webkit/Android.bp
index 94a4f50..f0ab143 100644
--- a/tests/uwb/Android.bp
+++ b/tests/tests/sdksandbox/webkit/Android.bp
@@ -1,4 +1,4 @@
-// Copyright (C) 2021 The Android Open Source Project
+// 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.
@@ -17,26 +17,30 @@
 }
 
 android_test {
-    name: "CtsUwbTestCases",
-    defaults: [
-        "cts_defaults",
-        "framework-uwb-cts-defaults",
+    name: "CtsSdkSandboxWebkitTestCases",
+    resource_dirs: ["res"],
+    defaults: ["cts_defaults"],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
     ],
-    // Tag this module as a cts test artifact
+    static_libs: [
+        "compatibility-device-util-axt",
+        "ctstestrunner-axt",
+        "CtsSdkSandboxTestScenario",
+        "ctswebkitsharedenv",
+    ],
+    data: [
+        ":EmptySdkProviderApp",
+        ":WebViewSandboxTestSdk",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.aidl",
+    ],
     test_suites: [
         "cts",
         "general-tests",
-        "mts-uwb",
     ],
-    libs: ["android.test.runner"],
-    static_libs: [
-        "androidx.test.ext.junit",
-        "ctstestrunner-axt",
-        "compatibility-device-util-axt",
-        "mockito-target-minus-junit4",
-        "com.uwb.support.fira",
-        "com.uwb.support.multichip",
-    ],
-    srcs: ["src/**/*.java"],
-    platform_apis: true,
+    sdk_version: "current",
 }
diff --git a/tests/tests/sdksandbox/webkit/AndroidManifest.xml b/tests/tests/sdksandbox/webkit/AndroidManifest.xml
new file mode 100644
index 0000000..24cc75f
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?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.sdksandbox.webkit.cts">
+
+    <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+
+    <application android:maxRecents="1">
+        <uses-library android:name="android.test.runner"/>
+        <uses-sdk-library android:name="com.android.emptysdkprovider"
+                          android:versionMajor="1"
+                          android:certDigest="0B:44:2D:88:FA:A7:B3:AD:23:8D:DE:29:8A:A1:9B:D5:62:03:92:0B:BF:D8:D3:EB:C8:99:33:2C:8E:E1:15:99"/>
+        <uses-sdk-library android:name="com.android.cts.sdk.webviewsandboxtest"
+                          android:versionMajor="1"
+                          android:certDigest="0B:44:2D:88:FA:A7:B3:AD:23:8D:DE:29:8A:A1:9B:D5:62:03:92:0B:BF:D8:D3:EB:C8:99:33:2C:8E:E1:15:99"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.sdksandbox.webkit.cts"
+                     android:label="CTS tests of android.sdksandbox.webkit">
+        <meta-data android:name="listener"
+                   android:value="com.android.cts.runner.CtsTestRunListener"/>
+    </instrumentation>
+
+</manifest>
diff --git a/tests/tests/sdksandbox/webkit/AndroidTest.xml b/tests/tests/sdksandbox/webkit/AndroidTest.xml
new file mode 100644
index 0000000..f0bf6b4
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/AndroidTest.xml
@@ -0,0 +1,41 @@
+<?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.
+-->
+<configuration description="Config for CTS SdkSandbox Webkit test cases">
+    <option name="test-suite-tag" value="cts"/>
+    <option name="config-descriptor:metadata" key="component" value="webview"/>
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="WebViewSandboxTestSdk.apk"/>
+        <option name="test-file-name" value="EmptySdkProviderApp.apk"/>
+        <option name="test-file-name" value="CtsSdkSandboxWebkitTestCases.apk"/>
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.sdksandbox.webkit.cts"/>
+        <option name="runtime-hint" value="6m39s"/>
+    </test>
+
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="cmd sdk_sandbox set-state --enabled"/>
+        <option name="run-command" value="device_config set_sync_disabled_for_tests persistent" />
+        <option name="teardown-command" value="cmd sdk_sandbox set-state --reset"/>
+        <option name="teardown-command" value="device_config set_sync_disabled_for_tests none" />
+    </target_preparer>
+
+</configuration>
diff --git a/tests/tests/sdksandbox/webkit/OWNERS b/tests/tests/sdksandbox/webkit/OWNERS
new file mode 100644
index 0000000..e59f9b2
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 76427
+include /tests/tests/webkit/OWNERS
diff --git a/tests/tests/webkit/res/raw/trustedcert.crt b/tests/tests/sdksandbox/webkit/res/raw/trustedcert.crt
similarity index 100%
copy from tests/tests/webkit/res/raw/trustedcert.crt
copy to tests/tests/sdksandbox/webkit/res/raw/trustedcert.crt
diff --git a/tests/tests/webkit/res/raw/trustedkey.der b/tests/tests/sdksandbox/webkit/res/raw/trustedkey.der
similarity index 100%
copy from tests/tests/webkit/res/raw/trustedkey.der
copy to tests/tests/sdksandbox/webkit/res/raw/trustedkey.der
Binary files differ
diff --git a/tests/tests/webkit/res/raw/untrustedcert.crt b/tests/tests/sdksandbox/webkit/res/raw/untrustedcert.crt
similarity index 100%
copy from tests/tests/webkit/res/raw/untrustedcert.crt
copy to tests/tests/sdksandbox/webkit/res/raw/untrustedcert.crt
diff --git a/tests/tests/webkit/res/raw/untrustedkey.der b/tests/tests/sdksandbox/webkit/res/raw/untrustedkey.der
similarity index 100%
copy from tests/tests/webkit/res/raw/untrustedkey.der
copy to tests/tests/sdksandbox/webkit/res/raw/untrustedkey.der
Binary files differ
diff --git a/hostsidetests/multidevices/uwb/snippet/Android.bp b/tests/tests/sdksandbox/webkit/sdk/Android.bp
similarity index 66%
rename from hostsidetests/multidevices/uwb/snippet/Android.bp
rename to tests/tests/sdksandbox/webkit/sdk/Android.bp
index 2ad7dd2..625d304 100644
--- a/hostsidetests/multidevices/uwb/snippet/Android.bp
+++ b/tests/tests/sdksandbox/webkit/sdk/Android.bp
@@ -16,20 +16,22 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-android_test {
-    name: "uwb_snippet",
-    sdk_version: "system_current",
-    srcs: [
-        "UwbManagerSnippet.java",
-    ],
+android_test_helper_app {
+    name: "WebViewSandboxTestSdk",
     manifest: "AndroidManifest.xml",
+    certificate: ":sdksandbox-test",
+    srcs: [
+        "src/**/*.java",
+    ],
     static_libs: [
-        "androidx.test.runner",
-        "guava",
-        "mobly-snippet-lib",
-        "com.uwb.support.ccc",
-        "com.uwb.support.fira",
-        "com.uwb.support.generic",
-        "com.uwb.support.multichip",
+        "androidx.test.ext.junit",
+        "CtsSdkSandboxTestRunner",
+        "compatibility-device-util-axt",
+        "ctsdeviceutillegacy-axt",
+        "ctswebkitsharedenv",
+        "CtsWebkitTestCasesSharedWithSdk",
+    ],
+    libs: [
+        "android.test.base",
     ],
 }
diff --git a/tests/tests/sdksandbox/webkit/sdk/AndroidManifest.xml b/tests/tests/sdksandbox/webkit/sdk/AndroidManifest.xml
new file mode 100644
index 0000000..e7944fc
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/sdk/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?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="com.android.cts.sdk.webviewsandboxtest">
+
+    <application>
+        <sdk-library android:name="com.android.cts.sdk.webviewsandboxtest"
+                     android:versionMajor="1"/>
+        <property android:name="android.sdksandbox.PROPERTY_SDK_PROVIDER_CLASS_NAME"
+                  android:value="com.android.cts.sdk.WebViewSandboxTestSdk"/>
+    </application>
+</manifest>
diff --git a/tests/tests/sdksandbox/webkit/sdk/src/com/android/cts/sdk/WebViewSandboxTestSdk.java b/tests/tests/sdksandbox/webkit/sdk/src/com/android/cts/sdk/WebViewSandboxTestSdk.java
new file mode 100644
index 0000000..91dda0f
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/sdk/src/com/android/cts/sdk/WebViewSandboxTestSdk.java
@@ -0,0 +1,104 @@
+/*
+ * 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.sdk;
+
+import android.app.sdksandbox.SandboxedSdk;
+import android.app.sdksandbox.testutils.testscenario.ISdkSandboxTestExecutor;
+import android.app.sdksandbox.testutils.testscenario.SdkSandboxTestScenarioRunner;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.webkit.WebView;
+import android.webkit.cts.IHostAppInvoker;
+import android.webkit.cts.SharedWebViewTest;
+import android.webkit.cts.SharedWebViewTestEnvironment;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+public class WebViewSandboxTestSdk extends SdkSandboxTestScenarioRunner {
+    private static final String TAG = WebViewSandboxTestSdk.class.getName();
+
+    private @Nullable SharedWebViewTest mTestInstance;
+
+    @Override
+    public SandboxedSdk onLoadSdk(Bundle params) {
+        try {
+            Bundle setupParams = params.getBundle(ISdkSandboxTestExecutor.TEST_SETUP_PARAMS);
+            if (setupParams != null) {
+                String webViewTestClassName =
+                        setupParams.getString(SharedWebViewTest.WEB_VIEW_TEST_CLASS_NAME);
+                mTestInstance = (SharedWebViewTest) Class.forName(webViewTestClassName)
+                        .newInstance();
+                setTestInstance(mTestInstance);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        return super.onLoadSdk(params);
+    }
+
+    @Override
+    public View getView(Context windowContext, Bundle params, int width, int height) {
+        WebView webView = new WebView(getContext());
+        FrameLayout rootLayout = wrapWebViewInLayout(webView);
+
+        if (mTestInstance != null) {
+            SharedWebViewTestEnvironment testEnvironment =
+                    new SharedWebViewTestEnvironment.Builder()
+                            .setContext(getContext())
+                            .setWebView(webView)
+                            .setHostAppInvoker(
+                                    IHostAppInvoker.Stub.asInterface(getCustomInterface()))
+                            .setRootLayout(rootLayout)
+                            .build();
+
+            mTestInstance.setTestEnvironment(testEnvironment);
+        }
+
+        return rootLayout;
+    }
+
+    private FrameLayout wrapWebViewInLayout(WebView webView) {
+        // Some tests add content views the root view of the activity which
+        // is a FrameLayout, hence adding a FrameLayout as a root here as well
+        FrameLayout rootLayout = new FrameLayout(getContext());
+        rootLayout.setLayoutParams(
+                new FrameLayout.LayoutParams(
+                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+        // Some tests expect the WebView to have a parent so making the parent
+        // a linear layout the same as the regular webkit tests.
+        LinearLayout webviewParent = new LinearLayout(getContext());
+        webviewParent.setLayoutParams(
+                new LinearLayout.LayoutParams(
+                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+        webviewParent.setOrientation(LinearLayout.VERTICAL);
+
+        webView.setLayoutParams(
+                new LinearLayout.LayoutParams(
+                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+        webviewParent.addView(webView);
+        rootLayout.addView(webviewParent);
+
+        return rootLayout;
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkMimeTypeMapTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkMimeTypeMapTest.java
new file mode 100644
index 0000000..4834cb3
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkMimeTypeMapTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class SdkMimeTypeMapTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.MimeTypeMapTest");
+
+    @Test
+    public void testGetFileExtensionFromUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetFileExtensionFromUrl");
+    }
+
+    @Test
+    public void testHasMimeType() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testHasMimeType");
+    }
+
+    @Test
+    public void testGetMimeTypeFromExtension() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetMimeTypeFromExtension");
+    }
+
+    @Test
+    public void testHasExtension() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testHasExtension");
+    }
+
+    @Test
+    public void testGetExtensionFromMimeType() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetExtensionFromMimeType");
+    }
+
+    @Test
+    public void testGetSingleton() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetSingleton");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxCookieManagerTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxCookieManagerTest.java
new file mode 100644
index 0000000..8db9cdf
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxCookieManagerTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxCookieManagerTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.CookieManagerTest");
+
+    @Test
+    public void testGetInstance() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetInstance");
+    }
+
+    @Test
+    public void testFlush() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testFlush");
+    }
+
+    @Test
+    public void testAcceptCookie() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAcceptCookie");
+    }
+
+    @Test
+    public void testSetCookie() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetCookie");
+    }
+
+    @Test
+    public void testSetCookieNullCallback() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetCookieNullCallback");
+    }
+
+    @Test
+    public void testSetCookieCallback() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetCookieCallback");
+    }
+
+    @Test
+    public void testRemoveCookies() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testRemoveCookies");
+    }
+
+    @Test
+    public void testRemoveCookiesNullCallback() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testRemoveCookiesNullCallback");
+    }
+
+    @Test
+    public void testRemoveCookiesCallback() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testRemoveCookiesCallback");
+    }
+
+    @Test
+    public void testThirdPartyCookie() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testThirdPartyCookie");
+    }
+
+    @Test
+    public void testSameSiteLaxByDefault() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSameSiteLaxByDefault");
+    }
+
+    @Test
+    public void testSameSiteNoneRequiresSecure() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSameSiteNoneRequiresSecure");
+    }
+
+    @Test
+    public void testSchemefulSameSite() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSchemefulSameSite");
+    }
+
+    @Test
+    public void testb3167208() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testb3167208");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxCookieTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxCookieTest.java
new file mode 100644
index 0000000..52f4daf
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxCookieTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxCookieTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.CookieTest");
+
+    @Presubmit
+    @Test
+    public void testDomain() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testDomain");
+    }
+
+    @Test
+    public void testSubDomain() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSubDomain");
+    }
+
+    @Test
+    public void testInvalidDomain() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testInvalidDomain");
+    }
+
+    @Test
+    public void testPath() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testPath");
+    }
+
+    @Test
+    public void testEmptyValue() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testEmptyValue");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxDateSorterTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxDateSorterTest.java
new file mode 100644
index 0000000..e83a524
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxDateSorterTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxDateSorterTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.DateSorterTest");
+
+    @Test
+    public void testConstructor() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testConstructor");
+    }
+
+    @Test
+    public void testGetLabel() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetLabel");
+    }
+
+    @Test
+    public void testGetIndex() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetIndex");
+    }
+
+    @Test
+    public void testGetBoundary() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetBoundary");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxHttpAuthHandlerTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxHttpAuthHandlerTest.java
new file mode 100644
index 0000000..05e9a1f
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxHttpAuthHandlerTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@AppModeFull
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxHttpAuthHandlerTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.HttpAuthHandlerTest");
+
+    @Test
+    public void testProceed() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testProceed");
+    }
+
+    @Test
+    public void testCancel() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testCancel");
+    }
+
+    @Test
+    public void testUseHttpAuthUsernamePassword() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testUseHttpAuthUsernamePassword");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxPostMessageTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxPostMessageTest.java
new file mode 100644
index 0000000..214084a
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxPostMessageTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxPostMessageTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.PostMessageTest");
+
+    @Test
+    public void testSimpleMessageToMainFrame() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSimpleMessageToMainFrame");
+    }
+
+    @Test
+    public void testWildcardOriginMatchesAnything() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testWildcardOriginMatchesAnything");
+    }
+
+    @Test
+    public void testEmptyStringOriginMatchesAnything() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testEmptyStringOriginMatchesAnything");
+    }
+
+    @Test
+    public void testMultipleMessagesToMainFrame() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testMultipleMessagesToMainFrame");
+    }
+
+    @Test
+    public void testMessageChannel() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testMessageChannel");
+    }
+
+    @Test
+    public void testClose() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testClose");
+    }
+
+    @Test
+    public void testReceiveMessagePort() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testReceiveMessagePort");
+    }
+
+    @Test
+    public void testWebMessageHandler() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testWebMessageHandler");
+    }
+
+    @Test
+    public void testWebMessageDefaultHandler() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testWebMessageDefaultHandler");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxServiceWorkerClientTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxServiceWorkerClientTest.java
new file mode 100644
index 0000000..39922ed
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxServiceWorkerClientTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxServiceWorkerClientTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.ServiceWorkerClientTest");
+
+    @Test
+    public void testServiceWorkerClientInterceptCallback() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testServiceWorkerClientInterceptCallback");
+    }
+
+    @Test
+    public void testSetNullServiceWorkerClient() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetNullServiceWorkerClient");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxURLUtilTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxURLUtilTest.java
new file mode 100644
index 0000000..4e047c7
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxURLUtilTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxURLUtilTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.URLUtilTest");
+
+    @Test
+    public void testIsAssetUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsAssetUrl");
+    }
+
+    @Test
+    public void testIsAboutUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsAboutUrl");
+    }
+
+    @Test
+    public void testIsContentUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsContentUrl");
+    }
+
+    @Test
+    public void testIsCookielessProxyUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsCookielessProxyUrl");
+    }
+
+    @Test
+    public void testIsDataUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsDataUrl");
+    }
+
+    @Test
+    public void testIsFileUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsFileUrl");
+    }
+
+    @Test
+    public void testIsHttpsUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsHttpsUrl");
+    }
+
+    @Test
+    public void testIsHttpUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsHttpUrl");
+    }
+
+    @Test
+    public void testIsJavaScriptUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsJavaScriptUrl");
+    }
+
+    @Test
+    public void testIsNetworkUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsNetworkUrl");
+    }
+
+    @Test
+    public void testIsValidUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIsValidUrl");
+    }
+
+    @Test
+    public void testComposeSearchUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testComposeSearchUrl");
+    }
+
+    @Test
+    public void testDecode() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testDecode");
+    }
+
+    @Test
+    public void testGuessFileName() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGuessFileName");
+    }
+
+    @Test
+    public void testGuessUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGuessUrl");
+    }
+
+    @Test
+    public void testStripAnchor() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testStripAnchor");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebBackForwardListTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebBackForwardListTest.java
new file mode 100644
index 0000000..7d8c85b
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebBackForwardListTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebBackForwardListTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebBackForwardListTest");
+
+    @Test
+    public void testGetCurrentItem() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetCurrentItem");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebChromeClientTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebChromeClientTest.java
new file mode 100644
index 0000000..95ec8da
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebChromeClientTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebChromeClientTest {
+
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebChromeClientTest");
+
+    @Test
+    public void testOnProgressChanged() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnProgressChanged");
+    }
+
+    @Test
+    public void testOnReceivedTitle() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedTitle");
+    }
+
+    @Test
+    public void testOnReceivedIcon() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedIcon");
+    }
+
+    @Test
+    public void testWindows() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testWindows");
+    }
+
+    @Test
+    public void testBlockWindowsSync() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testBlockWindowsSync");
+    }
+
+    @Test
+    public void testBlockWindowsAsync() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testBlockWindowsAsync");
+    }
+
+    @Test
+    public void testOnJsAlert() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnJsAlert");
+    }
+
+    @Test
+    public void testOnJsConfirm() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnJsConfirm");
+    }
+
+    @Test
+    public void testOnJsPrompt() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnJsPrompt");
+    }
+
+    @Test
+    public void testOnConsoleMessage() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnConsoleMessage");
+    }
+
+    @Test
+    public void testOnJsBeforeUnloadIsCalled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnJsBeforeUnloadIsCalled");
+    }
+
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebHistoryItemTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebHistoryItemTest.java
new file mode 100644
index 0000000..5eb7225
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebHistoryItemTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebHistoryItemTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebHistoryItemTest");
+
+    @Test
+    public void testWebHistoryItem() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testWebHistoryItem");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebSettingsTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebSettingsTest.java
new file mode 100644
index 0000000..e2112ae
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebSettingsTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebSettingsTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebSettingsTest");
+
+    @Test
+    public void testUserAgentString_default() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testUserAgentString_default");
+    }
+
+    @Test
+    public void testUserAgentStringTest() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testUserAgentStringTest");
+    }
+
+    @Test
+    public void testAccessUserAgentString() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessUserAgentString");
+    }
+
+    @Test
+    public void testAccessCacheMode_defaultValue() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessCacheMode_defaultValue");
+    }
+
+    @Test
+    public void testAccessCacheMode_cacheElseNetwork() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessCacheMode_cacheElseNetwork");
+    }
+
+    @Test
+    public void testAccessCacheMode_noCache() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessCacheMode_noCache");
+    }
+
+    @Test
+    public void testAccessCacheMode_cacheOnly() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessCacheMode_cacheOnly");
+    }
+
+    @Test
+    public void testAccessCursiveFontFamily() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessCursiveFontFamily");
+    }
+
+    @Test
+    public void testAccessFantasyFontFamily() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessFantasyFontFamily");
+    }
+
+    @Test
+    public void testAccessFixedFontFamily() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessFixedFontFamily");
+    }
+
+    @Test
+    public void testAccessSansSerifFontFamily() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessSansSerifFontFamily");
+    }
+
+    @Test
+    public void testAccessSerifFontFamily() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessSerifFontFamily");
+    }
+
+    @Test
+    public void testAccessStandardFontFamily() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessStandardFontFamily");
+    }
+
+    @Test
+    public void testAccessDefaultFontSize() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessDefaultFontSize");
+    }
+
+    @Test
+    public void testAccessDefaultFixedFontSize() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessDefaultFixedFontSize");
+    }
+
+    @Test
+    public void testAccessDefaultTextEncodingName() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessDefaultTextEncodingName");
+    }
+
+    @Test
+    public void testAccessJavaScriptCanOpenWindowsAutomatically() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessJavaScriptCanOpenWindowsAutomatically");
+    }
+
+    @Test
+    public void testAccessJavaScriptEnabled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessJavaScriptEnabled");
+    }
+
+    @Test
+    public void testAccessLayoutAlgorithm() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessLayoutAlgorithm");
+    }
+
+    @Test
+    public void testAccessMinimumFontSize() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessMinimumFontSize");
+    }
+
+    @Test
+    public void testAccessMinimumLogicalFontSize() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessMinimumLogicalFontSize");
+    }
+
+    @Test
+    public void testAccessPluginsEnabled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessPluginsEnabled");
+    }
+
+    @Test
+    public void testOffscreenPreRaster() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOffscreenPreRaster");
+    }
+
+    @Test
+    public void testAccessPluginsPath() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessPluginsPath");
+    }
+
+    @Test
+    public void testAccessTextSize() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessTextSize");
+    }
+
+    @Test
+    public void testAccessUseDoubleTree() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessUseDoubleTree");
+    }
+
+    @Test
+    public void testAccessUseWideViewPort() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessUseWideViewPort");
+    }
+
+    @Test
+    public void testSetNeedInitialFocus() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetNeedInitialFocus");
+    }
+
+    @Test
+    public void testSetRenderPriority() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetRenderPriority");
+    }
+
+    @Test
+    public void testAccessSupportMultipleWindows() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessSupportMultipleWindows");
+    }
+
+    @Test
+    public void testAccessSupportZoom() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessSupportZoom");
+    }
+
+    @Test
+    public void testAccessBuiltInZoomControls() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessBuiltInZoomControls");
+    }
+
+    @Test
+    public void testAppCacheDisabled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAppCacheDisabled");
+    }
+
+    @Test
+    public void testAppCacheEnabled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAppCacheEnabled");
+    }
+
+    @Test
+    public void testDatabaseDisabled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testDatabaseDisabled");
+    }
+
+    @Test
+    public void testDisabledActionModeMenuItems() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testDisabledActionModeMenuItems");
+    }
+
+    @Test
+    public void testLoadsImagesAutomatically_default() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadsImagesAutomatically_default");
+    }
+
+    @Test
+    public void testLoadsImagesAutomatically_httpImagesLoaded() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadsImagesAutomatically_httpImagesLoaded");
+    }
+
+    @Test
+    public void testLoadsImagesAutomatically_dataUriImagesLoaded() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadsImagesAutomatically_dataUriImagesLoaded");
+    }
+
+    @Test
+    public void testLoadsImagesAutomatically_blockLoadingImages() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadsImagesAutomatically_blockLoadingImages");
+    }
+
+    @Test
+    public void testLoadsImagesAutomatically_loadImagesWithoutReload() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadsImagesAutomatically_loadImagesWithoutReload");
+    }
+
+    @Test
+    public void testBlockNetworkImage() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testBlockNetworkImage");
+    }
+
+    @Test
+    public void testBlockNetworkLoads() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testBlockNetworkLoads");
+    }
+
+    @Test
+    public void testIframesWhenAccessFromFileURLsDisabled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIframesWhenAccessFromFileURLsDisabled");
+    }
+
+    @Test
+    public void testXHRWhenAccessFromFileURLsEnabled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testXHRWhenAccessFromFileURLsEnabled");
+    }
+
+    @Test
+    public void testXHRWhenAccessFromFileURLsDisabled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testXHRWhenAccessFromFileURLsDisabled");
+    }
+
+    @Test
+    public void testAllowMixedMode() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAllowMixedMode");
+    }
+
+    @Test
+    public void testEnableSafeBrowsing() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testEnableSafeBrowsing");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewClientTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewClientTest.java
new file mode 100644
index 0000000..68b68ba
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewClientTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebViewClientTest {
+    // TODO(b/260196711): IME does not currently work correctly in the SDK RUntime. We should enable
+    // impacted tests once this is fixed.
+    // This prevents some tests from running.
+    private static final boolean CAN_INJECT_KEY_EVENTS = false;
+
+    // TODO(b/266051278): Uncomment this when we work out why preserving
+    // the SDK sandbox manager between tests cases {@link testOnRenderProcessGone}
+    // to fail.
+    //
+    // @ClassRule
+    // public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+    //         new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebViewClientTest");
+
+    @Test
+    public void testShouldOverrideUrlLoadingDefault() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testShouldOverrideUrlLoadingDefault");
+    }
+
+    @Test
+    public void testShouldOverrideUrlLoading() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testShouldOverrideUrlLoading");
+    }
+
+    @Test
+    public void testShouldOverrideUrlLoadingOnCreateWindow() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testShouldOverrideUrlLoadingOnCreateWindow");
+    }
+
+    @Test
+    public void testLoadPage() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadPage");
+    }
+
+    @Test
+    public void testOnReceivedLoginRequest() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedLoginRequest");
+    }
+
+    @Test
+    public void testOnReceivedError() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedError");
+    }
+
+    @Test
+    public void testOnReceivedErrorForSubresource() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedErrorForSubresource");
+    }
+
+    @Test
+    public void testOnReceivedHttpError() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedHttpError");
+    }
+
+    @Test
+    public void testOnFormResubmission() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnFormResubmission");
+    }
+
+    @Test
+    public void testDoUpdateVisitedHistory() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testDoUpdateVisitedHistory");
+    }
+
+    @Test
+    public void testOnReceivedHttpAuthRequest() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedHttpAuthRequest");
+    }
+
+    @Test
+    public void testShouldOverrideKeyEvent() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testShouldOverrideKeyEvent");
+    }
+
+    @Test
+    public void testOnUnhandledKeyEvent() throws Exception {
+        Assume.assumeTrue(CAN_INJECT_KEY_EVENTS);
+        sdkTester.assertSdkTestRunPasses("testOnUnhandledKeyEvent");
+    }
+
+    @Test
+    public void testOnScaleChanged() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnScaleChanged");
+    }
+
+    @Test
+    public void testShouldInterceptRequestParams() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testShouldInterceptRequestParams");
+    }
+
+    @Test
+    public void testShouldInterceptRequestResponse() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testShouldInterceptRequestResponse");
+    }
+
+    @Test
+    public void testOnRenderProcessGoneDefault() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnRenderProcessGoneDefault");
+    }
+
+    @Test
+    public void testOnRenderProcessGone() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnRenderProcessGone");
+    }
+
+    // TODO(crbug/1245351): Remove @FlakyTest once bug fixed
+    @FlakyTest
+    @Test
+    public void testOnSafeBrowsingHitBackToSafety() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnSafeBrowsingHitBackToSafety");
+    }
+
+    // TODO(crbug/1245351): Remove @FlakyTest once bug fixed
+    @FlakyTest
+    @Test
+    public void testOnSafeBrowsingHitProceed() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnSafeBrowsingHitProceed");
+    }
+
+    // TODO(crbug/1245351): Remove @FlakyTest once bug fixed
+    @FlakyTest
+    @Test
+    public void testOnSafeBrowsingMalwareCode() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnSafeBrowsingMalwareCode");
+    }
+
+    // TODO(crbug/1245351): Remove @FlakyTest once bug fixed
+    @FlakyTest
+    @Test
+    public void testOnSafeBrowsingPhishingCode() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnSafeBrowsingPhishingCode");
+    }
+
+    // TODO(crbug/1245351): Remove @FlakyTest once bug fixed
+    @FlakyTest
+    @Test
+    public void testOnSafeBrowsingUnwantedSoftwareCode() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnSafeBrowsingUnwantedSoftwareCode");
+    }
+
+    // TODO(crbug/1245351): Remove @FlakyTest once bug fixed
+    @FlakyTest
+    @Test
+    public void testOnSafeBrowsingBillingCode() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnSafeBrowsingBillingCode");
+    }
+
+    @Test
+    public void testOnPageCommitVisibleCalled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnPageCommitVisibleCalled");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewRenderProcessClientTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewRenderProcessClientTest.java
new file mode 100644
index 0000000..999ad44
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewRenderProcessClientTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@AppModeFull
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebViewRenderProcessClientTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebViewRenderProcessClientTest");
+
+    @Test
+    public void testWebViewRenderProcessClientWithoutExecutor() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testWebViewRenderProcessClientWithoutExecutor");
+    }
+
+    @Test
+    public void testWebViewRenderProcessClientWithExecutor() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testWebViewRenderProcessClientWithExecutor");
+    }
+
+    @Test
+    public void testSetWebViewRenderProcessClient() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetWebViewRenderProcessClient");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewSslTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewSslTest.java
new file mode 100644
index 0000000..ba5975e
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewSslTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebViewSslTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebViewSslTest");
+
+    @Test
+    @MediumTest
+    public void testInsecureSiteClearsCertificate() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testInsecureSiteClearsCertificate");
+    }
+
+    @Test
+    @MediumTest
+    public void testSecureSiteSetsCertificate() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSecureSiteSetsCertificate");
+    }
+
+    @Test
+    @MediumTest
+    public void testClearSslPreferences() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testClearSslPreferences");
+    }
+
+    @Test
+    @MediumTest
+    public void testOnReceivedSslError() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedSslError");
+    }
+
+    @Test
+    @MediumTest
+    public void testOnReceivedSslErrorProceed() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedSslErrorProceed");
+    }
+
+    @Test
+    @MediumTest
+    public void testOnReceivedSslErrorCancel() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testOnReceivedSslErrorCancel");
+    }
+
+    @Test
+    @MediumTest
+    public void testSslErrorProceedResponseReusedForSameHost() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSslErrorProceedResponseReusedForSameHost");
+    }
+
+    @Test
+    @MediumTest
+    public void testSslErrorProceedResponseNotReusedForDifferentHost() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSslErrorProceedResponseNotReusedForDifferentHost");
+    }
+
+    @Test
+    @MediumTest
+    public void testSecureServerRequestingClientCertDoesNotCancelRequest() throws Exception {
+        sdkTester.assertSdkTestRunPasses(
+                "testSecureServerRequestingClientCertDoesNotCancelRequest");
+    }
+
+    @Test
+    @MediumTest
+    public void testSecureServerRequiringClientCertDoesCancelRequest() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSecureServerRequiringClientCertDoesCancelRequest");
+    }
+
+    @Test
+    @MediumTest
+    public void testProceedClientCertRequest() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testProceedClientCertRequest");
+    }
+
+    @Test
+    @MediumTest
+    public void testIgnoreClientCertRequest() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testIgnoreClientCertRequest");
+    }
+
+    @Test
+    @MediumTest
+    public void testCancelClientCertRequest() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testCancelClientCertRequest");
+    }
+
+    @Test
+    @MediumTest
+    public void testClientCertIssuersReceivedCorrectly() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testClientCertIssuersReceivedCorrectly");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewTest.java
new file mode 100644
index 0000000..e6de7e8
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewTest.java
@@ -0,0 +1,403 @@
+/*
+ * 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Assume;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebViewTest {
+    // TODO(b/230340812): IME does not currently work correctly in the SDK RUntime. We should enable
+    // impacted tests once this is fixed.
+    // This prevents some tests from running.
+    private static final boolean CAN_INJECT_KEY_EVENTS = false;
+
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebViewTest");
+
+    @Test
+    public void testConstructor() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testConstructor");
+    }
+
+    @Test
+    public void testCreatingWebViewWithDeviceEncrpytionFails() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testCreatingWebViewWithDeviceEncrpytionFails");
+    }
+
+    @Test
+    public void testCreatingWebViewWithMultipleEncryptionContext() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testCreatingWebViewWithMultipleEncryptionContext");
+    }
+
+    @Test
+    public void testCreatingWebViewCreatesCookieSyncManager() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testCreatingWebViewCreatesCookieSyncManager");
+    }
+
+    @Test
+    public void testFindAddress() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testFindAddress");
+    }
+
+    @Test
+    public void testAccessHttpAuthUsernamePassword() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessHttpAuthUsernamePassword");
+    }
+
+    @Test
+    public void testWebViewDatabaseAccessHttpAuthUsernamePassword() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testWebViewDatabaseAccessHttpAuthUsernamePassword");
+    }
+
+    @Test
+    public void testScrollBarOverlay() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testScrollBarOverlay");
+    }
+
+    @Test
+    public void testFlingScroll() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testFlingScroll");
+    }
+
+    @Test
+    @Presubmit
+    public void testLoadUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadUrl");
+    }
+
+    @Test
+    public void testPostUrlWithNetworkUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testPostUrlWithNetworkUrl");
+    }
+
+    @Test
+    public void testAppInjectedXRequestedWithHeaderIsNotOverwritten() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAppInjectedXRequestedWithHeaderIsNotOverwritten");
+    }
+
+    @Test
+    public void testAppCanInjectHeadersViaImmutableMap() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAppCanInjectHeadersViaImmutableMap");
+    }
+
+    @Test
+    public void testCanInjectHeaders() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testCanInjectHeaders");
+    }
+
+    @Test
+    public void testGetVisibleTitleHeight() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetVisibleTitleHeight");
+    }
+
+    @Test
+    public void testGetOriginalUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetOriginalUrl");
+    }
+
+    @Test
+    public void testStopLoading() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testStopLoading");
+    }
+
+    @Test
+    public void testGoBackAndForward() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGoBackAndForward");
+    }
+
+    @Test
+    public void testAddJavascriptInterface() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAddJavascriptInterface");
+    }
+
+    @Test
+    public void testAddJavascriptInterfaceNullObject() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAddJavascriptInterfaceNullObject");
+    }
+
+    @Test
+    public void testRemoveJavascriptInterface() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testRemoveJavascriptInterface");
+    }
+
+    @Test
+    public void testUseRemovedJavascriptInterface() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testUseRemovedJavascriptInterface");
+    }
+
+    @Test
+    public void testAddJavascriptInterfaceExceptions() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAddJavascriptInterfaceExceptions");
+    }
+
+    @Test
+    public void testJavascriptInterfaceCustomPropertiesClearedOnReload() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testJavascriptInterfaceCustomPropertiesClearedOnReload");
+    }
+
+    @Test
+    @MediumTest
+    public void testJavascriptInterfaceForClientPopup() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testJavascriptInterfaceForClientPopup");
+    }
+
+    @Test
+    @MediumTest
+    public void testLoadDataWithBaseUrl_historyUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadDataWithBaseUrl_historyUrl");
+    }
+
+    @Test
+    public void testLoadDataWithBaseUrl_nullHistoryUrlShowsAsAboutBlank() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadDataWithBaseUrl_nullHistoryUrlShowsAsAboutBlank");
+    }
+
+    @Test
+    public void testLoadDataWithBaseUrl_dataBaseUrlIgnoresHistoryUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadDataWithBaseUrl_dataBaseUrlIgnoresHistoryUrl");
+    }
+
+    @Test
+    public void testLoadDataWithBaseUrl_unencodedContentHttpBaseUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadDataWithBaseUrl_unencodedContentHttpBaseUrl");
+    }
+
+    @Test
+    public void testLoadDataWithBaseUrl_urlEncodedContentDataBaseUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadDataWithBaseUrl_urlEncodedContentDataBaseUrl");
+    }
+
+    @Test
+    public void testLoadDataWithBaseUrl_nullSafe() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadDataWithBaseUrl_nullSafe");
+    }
+
+    @Test
+    public void testSaveWebArchive() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSaveWebArchive");
+    }
+
+    @Test
+    public void testFindAll() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testFindAll");
+    }
+
+    @Test
+    public void testFindNext() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testFindNext");
+    }
+
+    @Test
+    public void testPageScroll() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testPageScroll");
+    }
+
+    @Test
+    public void testGetContentHeight() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetContentHeight");
+    }
+
+    @Test
+    public void testPlatformNotifications() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testPlatformNotifications");
+    }
+
+    @Test
+    public void testAccessPluginList() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessPluginList");
+    }
+
+    @Test
+    public void testDestroy() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testDestroy");
+    }
+
+    @Test
+    public void testDebugDump() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testDebugDump");
+    }
+
+    @Test
+    public void testSetInitialScale() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetInitialScale");
+    }
+
+    @Test
+    public void testRequestChildRectangleOnScreen() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testRequestChildRectangleOnScreen");
+    }
+
+    @Test
+    public void testSetLayoutParams() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetLayoutParams");
+    }
+
+    @Test
+    public void testSetMapTrackballToArrowKeys() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetMapTrackballToArrowKeys");
+    }
+
+    @Test
+    public void testPauseResumeTimers() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testPauseResumeTimers");
+    }
+
+    @Test
+    public void testEvaluateJavascript() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testEvaluateJavascript");
+    }
+
+    @Test
+    public void testPrinting() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testPrinting");
+    }
+
+    @Test
+    public void testPrintingPagesCount() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testPrintingPagesCount");
+    }
+
+    @Test
+    public void testVisualStateCallbackCalled() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testVisualStateCallbackCalled");
+    }
+
+    @Test
+    public void testSetSafeBrowsingAllowlistWithMalformedList() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetSafeBrowsingAllowlistWithMalformedList");
+    }
+
+    @Test
+    public void testSetSafeBrowsingAllowlistWithValidList() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetSafeBrowsingAllowlistWithValidList");
+    }
+
+    @Test
+    public void testGetWebViewClient() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetWebViewClient");
+    }
+
+    @Test
+    public void testGetWebChromeClient() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetWebChromeClient");
+    }
+
+    @Test
+    public void testSetCustomTextClassifier() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetCustomTextClassifier");
+    }
+
+    @Test
+    public void testGetSafeBrowsingPrivacyPolicyUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetSafeBrowsingPrivacyPolicyUrl");
+    }
+
+    @Test
+    public void testWebViewClassLoaderReturnsNonNull() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testWebViewClassLoaderReturnsNonNull");
+    }
+
+    @Test
+    public void testCapturePicture() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testCapturePicture");
+    }
+
+    @Test
+    public void testSetPictureListener() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetPictureListener");
+    }
+
+    @Test
+    public void testLoadData() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadData");
+    }
+
+    @Test
+    public void testLoadDataWithBaseUrl_resolvesRelativeToBaseUrl() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadDataWithBaseUrl_resolvesRelativeToBaseUrl");
+    }
+
+    @Test
+    public void testLoadDataWithBaseUrl_javascriptCanAccessOrigin() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testLoadDataWithBaseUrl_javascriptCanAccessOrigin");
+    }
+
+    @Test
+    public void testDocumentHasImages() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testDocumentHasImages");
+    }
+
+    @Test
+    public void testClearHistory() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testClearHistory");
+    }
+
+    @Test
+    public void testSaveAndRestoreState() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSaveAndRestoreState");
+    }
+
+    @Test
+    public void testSetDownloadListener() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetDownloadListener");
+    }
+
+    @Test
+    public void testSetNetworkAvailable() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetNetworkAvailable");
+    }
+
+    @Test
+    public void testSetWebChromeClient() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testSetWebChromeClient");
+    }
+
+    @Test
+    public void testRequestFocusNodeHref() throws Exception {
+        Assume.assumeTrue(CAN_INJECT_KEY_EVENTS);
+        sdkTester.assertSdkTestRunPasses("testRequestFocusNodeHref");
+    }
+
+    @Test
+    public void testRequestImageRef() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testRequestImageRef");
+    }
+
+    @Test
+    public void testGetHitTestResult() throws Exception {
+        Assume.assumeTrue(CAN_INJECT_KEY_EVENTS);
+        sdkTester.assertSdkTestRunPasses("testGetHitTestResult");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewTransportTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewTransportTest.java
new file mode 100644
index 0000000..a4db755
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewTransportTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebViewTransportTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebViewTransportTest");
+
+    @Test
+    public void testAccessWebView() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAccessWebView");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewZoomTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewZoomTest.java
new file mode 100644
index 0000000..4afee9c
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkSandboxWebViewZoomTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SdkSandboxWebViewZoomTest {
+
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebViewZoomTest");
+
+    @Test
+    public void testZoomIn() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testZoomIn");
+    }
+
+    @Test
+    public void testGetZoomControls() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetZoomControls");
+    }
+
+    @Test
+    public void testInvokeZoomPicker() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testInvokeZoomPicker");
+    }
+
+    @Test
+    public void testZoom_canNotZoomInPastMaximum() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testZoom_canNotZoomInPastMaximum");
+    }
+
+    @Test
+    public void testZoom_canNotZoomOutPastMinimum() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testZoom_canNotZoomOutPastMinimum");
+    }
+
+    @Test
+    public void testCanZoomWhileZoomSupportedFalse() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testCanZoomWhileZoomSupportedFalse");
+    }
+
+    @Test
+    public void testZoomByPowerOfTwoIncrements() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testZoomByPowerOfTwoIncrements");
+    }
+
+    @Test
+    public void testZoomByNonPowerOfTwoIncrements() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testZoomByNonPowerOfTwoIncrements");
+    }
+
+    @Test
+    public void testScaleChangeCallbackMatchesGetScale() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testScaleChangeCallbackMatchesGetScale");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkServiceWorkerWebSettingsTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkServiceWorkerWebSettingsTest.java
new file mode 100644
index 0000000..e83a333
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkServiceWorkerWebSettingsTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class SdkServiceWorkerWebSettingsTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.ServiceWorkerWebSettingsTest");
+
+    @Test
+    public void testCacheMode() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testCacheMode");
+    }
+
+    @Test
+    public void testAllowContentAccess() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAllowContentAccess");
+    }
+
+    @Test
+    public void testAllowFileAccess() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testAllowFileAccess");
+    }
+
+    @Test
+    public void testBlockNetworkLoads() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testBlockNetworkLoads");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkWebViewRenderProcessTest.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkWebViewRenderProcessTest.java
new file mode 100644
index 0000000..91f3fbe
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/SdkWebViewRenderProcessTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.sdksandbox.webkit.cts;
+
+import android.app.sdksandbox.testutils.testscenario.KeepSdkSandboxAliveRule;
+import android.platform.test.annotations.AppModeFull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@AppModeFull
+@RunWith(AndroidJUnit4.class)
+public class SdkWebViewRenderProcessTest {
+    @ClassRule
+    public static final KeepSdkSandboxAliveRule sSdkTestSuiteSetup =
+            new KeepSdkSandboxAliveRule("com.android.emptysdkprovider");
+
+    @Rule
+    public final WebViewSandboxTestRule sdkTester =
+            new WebViewSandboxTestRule("android.webkit.cts.WebViewRenderProcessTest");
+
+    @Test
+    public void testGetWebViewRenderProcess() throws Exception {
+        sdkTester.assertSdkTestRunPasses("testGetWebViewRenderProcess");
+    }
+}
diff --git a/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/WebViewSandboxTestRule.java b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/WebViewSandboxTestRule.java
new file mode 100644
index 0000000..433b5ce
--- /dev/null
+++ b/tests/tests/sdksandbox/webkit/src/android/sdksandbox/webkit/cts/WebViewSandboxTestRule.java
@@ -0,0 +1,63 @@
+/*
+ * 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.sdksandbox.webkit.cts;
+
+import static android.app.sdksandbox.testutils.testscenario.SdkSandboxScenarioRule.ENABLE_LIFE_CYCLE_ANNOTATIONS;
+
+import android.app.sdksandbox.testutils.testscenario.SdkSandboxScenarioRule;
+import android.os.Bundle;
+import android.webkit.cts.SharedWebViewTest;
+import android.webkit.cts.SharedWebViewTestEnvironment;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.compatibility.common.util.NullWebViewUtils;
+
+import org.junit.Assume;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * This rule is used to invoke webview tests inside a test sdk.
+ * This rule is a wrapper for using the
+ * {@link WebViewSandboxTestSdk}, for detailed implementation
+ * details please refer to its parent class
+ * {@link SdkSandboxScenarioRule}.
+ */
+public class WebViewSandboxTestRule extends SdkSandboxScenarioRule {
+
+    public WebViewSandboxTestRule(String webViewTestClassName) {
+        super(
+                "com.android.cts.sdk.webviewsandboxtest",
+                getSetupParams(webViewTestClassName),
+                SharedWebViewTestEnvironment.createHostAppInvoker(
+                        ApplicationProvider.getApplicationContext(), true),
+                ENABLE_LIFE_CYCLE_ANNOTATIONS);
+    }
+
+    private static Bundle getSetupParams(String webViewTestClassName) {
+        Bundle params = new Bundle();
+        params.putString(SharedWebViewTest.WEB_VIEW_TEST_CLASS_NAME, webViewTestClassName);
+        return params;
+    }
+
+    @Override
+    public Statement apply(final Statement base, final Description description) {
+        // This will prevent shared webview tests from running if a WebView provider does not exist.
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+        return super.apply(base, description);
+    }
+}
diff --git a/tests/tests/security/Android.bp b/tests/tests/security/Android.bp
index 4d80151..ad8d509 100644
--- a/tests/tests/security/Android.bp
+++ b/tests/tests/security/Android.bp
@@ -33,6 +33,7 @@
         "compatibility-common-util-devicesidelib",
         "guava",
         "platform-test-annotations",
+        "permission-test-util-lib",
         "sts-device-util",
         "hamcrest-library",
         "NeneInternal",
@@ -81,6 +82,16 @@
         ":CtsDeviceInfo",
         ":RolePermissionOverrideTestApp",
         ":SplitBluetoothPermissionTestApp",
+        ":CtsPermissionBackupAppCert1",
+        ":CtsPermissionBackupAppCert1Dup",
+        ":CtsPermissionBackupAppCert2",
+        ":CtsPermissionBackupAppCert3",
+        ":CtsPermissionBackupAppCert4",
+        ":CtsPermissionBackupAppCert12",
+        ":CtsPermissionBackupAppCert12Dup",
+        ":CtsPermissionBackupAppCert34",
+        ":CtsPermissionBackupAppCert123",
+        ":CtsPermissionBackupAppCert4History124",
         ":CtsAppThatRequestCustomCameraPermission",
         ":CtsWallpaperBalTestApp",
     ],
@@ -92,6 +103,115 @@
     manifest: "testdata/packageinstallertestapp.xml",
 }
 
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert1",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-1",
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert1Dup",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-1",
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert2",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-2",
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert3",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-3",
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert4",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-4",
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert12",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-1",
+    additional_certificates: [
+        ":permission-test-cert-2",
+    ],
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert12Dup",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-1",
+    additional_certificates: [
+        ":permission-test-cert-2",
+    ],
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert34",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-3",
+    additional_certificates: [
+        ":permission-test-cert-4",
+    ],
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert123",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-1",
+    additional_certificates: [
+        ":permission-test-cert-2",
+        ":permission-test-cert-3",
+    ],
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+    name: "CtsPermissionBackupAppCert4History124",
+    min_sdk_version: "30",
+    resource_dirs: [],
+    asset_dirs: [],
+    certificate: ":permission-test-cert-4",
+    additional_certificates: [
+        ":permission-test-cert-1",
+
+    ],
+    rotationMinSdkVersion: "30",
+    lineage: ":permission-test-cert-with-rotation-history",
+    manifest: "testdata/permissionbackuptestapp/AndroidManifest.xml",
+}
+
 android_app_certificate {
     name: "security_cts_test_certificate",
     certificate: "security_cts_test_cert",
@@ -99,5 +219,34 @@
 
 android_test_helper_app {
     name: "RolePermissionOverrideTestApp",
+    resource_dirs: [],
+    asset_dirs: [],
     manifest: "testdata/rolepermissionoverridetestapp.xml",
 }
+
+android_app_certificate {
+    name: "permission-test-cert-1",
+    certificate: "test-cert-1",
+}
+
+android_app_certificate {
+    name: "permission-test-cert-2",
+    certificate: "test-cert-2",
+}
+
+android_app_certificate {
+    name: "permission-test-cert-3",
+    certificate: "test-cert-3",
+}
+
+android_app_certificate {
+    name: "permission-test-cert-4",
+    certificate: "test-cert-4",
+}
+
+filegroup {
+    name: "permission-test-cert-with-rotation-history",
+    srcs: [
+        "test-cert-with-1-2-4-in-rotation-history",
+    ],
+}
diff --git a/tests/tests/security/AndroidTest.xml b/tests/tests/security/AndroidTest.xml
index 7f972b3..20ddcf5 100644
--- a/tests/tests/security/AndroidTest.xml
+++ b/tests/tests/security/AndroidTest.xml
@@ -53,6 +53,16 @@
         <option name="cleanup" value="true" />
         <option name="push" value="RolePermissionOverrideTestApp.apk->/data/local/tmp/cts/security/RolePermissionOverrideTestApp.apk" />
         <option name="push" value="SplitBluetoothPermissionTestApp.apk->/data/local/tmp/cts/security/SplitBluetoothPermissionTestApp.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert1.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert1.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert1Dup.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert1Dup.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert2.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert2.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert3.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert3.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert4.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert4.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert12.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert12.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert12Dup.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert12Dup.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert123.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert123.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert34.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert34.apk" />
+        <option name="push" value="CtsPermissionBackupAppCert4History124.apk->/data/local/tmp/cts/security/CtsPermissionBackupAppCert4History124.apk" />
         <option name="push" value="CtsAppThatRequestCustomCameraPermission.apk->/data/local/tmp/cts/permissions/CtsAppThatRequestCustomCameraPermission.apk" />
     </target_preparer>
 
@@ -63,4 +73,9 @@
         <option name="test-timeout" value="900000" />
         <option name="hidden-api-checks" value="false" />
     </test>
+
+    <target_preparer class="android.cts.backup.BackupPreparer">
+        <option name="enable-backup-if-needed" value="true" />
+        <option name="select-local-transport" value="true" />
+    </target_preparer>
 </configuration>
diff --git a/tests/tests/security/src/android/security/cts/PermissionBackupCertificateCheckTest.kt b/tests/tests/security/src/android/security/cts/PermissionBackupCertificateCheckTest.kt
new file mode 100644
index 0000000..1170939
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/PermissionBackupCertificateCheckTest.kt
@@ -0,0 +1,813 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package android.security.cts
+
+import android.Manifest.permission.*
+import android.app.AppOpsManager
+import android.content.pm.PackageManager.*
+import android.os.ParcelFileDescriptor
+import android.permission.cts.PermissionUtils.grantPermission
+import android.platform.test.annotations.AppModeFull
+import android.platform.test.annotations.AsbSecurityTest
+import androidx.test.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compatibility.common.util.BackupUtils
+import com.android.compatibility.common.util.BackupUtils.LOCAL_TRANSPORT_TOKEN
+import com.android.compatibility.common.util.BusinessLogicTestCase
+import com.android.compatibility.common.util.ShellUtils.runShellCommand
+import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.sts.common.util.StsExtraBusinessLogicTestCase
+import java.io.InputStream
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests that permissions for backed up apps are restored only after checking that their signing
+ * certificates are compared.
+ *
+ * @see [com.android.permissioncontroller.permission.service.BackupHelper]
+ */
+@AppModeFull
+@RunWith(AndroidJUnit4::class)
+class PermissionBackupCertificateCheckTest : StsExtraBusinessLogicTestCase() {
+    private val backupUtils: BackupUtils =
+        object : BackupUtils() {
+            override fun executeShellCommand(command: String): InputStream {
+                val pfd =
+                    BusinessLogicTestCase.getInstrumentation()
+                        .uiAutomation
+                        .executeShellCommand(command)
+                return ParcelFileDescriptor.AutoCloseInputStream(pfd)
+            }
+        }
+
+    private var isBackupSupported = false
+
+    private val targetContext = InstrumentationRegistry.getTargetContext()
+
+    @Before
+    fun setUp() {
+        val packageManager = BusinessLogicTestCase.getInstrumentation().context.packageManager
+        isBackupSupported =
+            (packageManager != null && packageManager.hasSystemFeature(FEATURE_BACKUP))
+
+        if (isBackupSupported) {
+            assertTrue("Backup not enabled", backupUtils.isBackupEnabled)
+            assertTrue("LocalTransport not selected", backupUtils.isLocalTransportSelected)
+            backupUtils.executeShellCommandSync("setprop log.tag.$APP_LOG_TAG VERBOSE")
+        }
+    }
+
+    @After
+    fun tearDown() {
+        uninstallIfInstalled(APP)
+        clearFlag(APP, ACCESS_FINE_LOCATION, FLAG_PERMISSION_USER_SET)
+        clearFlag(APP, ACCESS_BACKGROUND_LOCATION, FLAG_PERMISSION_USER_SET)
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the app being restored has the
+     * same certificate as the backed up app.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_sameCert_restoresRuntimePermissions() {
+        install(APP_APK_CERT_1)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_1_DUP)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the app being restored has a
+     * different certificate as the backed up app.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_diffCert_doesNotGrantRuntimePermissions() {
+        install(APP_APK_CERT_1)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_3)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the app being restored has the
+     * backed up app's certificate in its signing history.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_midHistoryToRotated_restoresRuntimePermissions() {
+        install(APP_APK_CERT_2)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the app being restored has the
+     * backed up app's certificate as the original certificate in its signing history.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_origToRotated_restoresRuntimePermissions() {
+        install(APP_APK_CERT_1)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the backed up app has the
+     * restored app's certificate in its signing history.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_rotatedToMidHistory_restoresRuntimePermissions() {
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_2)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the backed up app has the
+     * restored app's certificate in its signing history as its original certificate.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_rotatedToOrig_restoresRuntimePermissions() {
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_1)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the backed up app has the same
+     * certificate as the restored app, but the restored app additionally has signing certificate
+     * history.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_sameWithHistory_restoresRuntimePermissions() {
+        install(APP_APK_CERT_4)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the backed up app has the same
+     * certificate as the restored app, but the backed up app additionally has signing certificate
+     * history.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_sameWithoutHistory_restoresRuntimePermissions() {
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the app being restored has
+     * signing history, but the backed up app's certificate is not in this signing history.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_notInBackedUpHistory_doesNotRestoreRuntimePerms() {
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_3)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the app being restored has
+     * signing history, but the backed up app's certificate is not in this signing history.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_notInRestoredHistory_doesNotRestoreRuntimePerms() {
+        install(APP_APK_CERT_3)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the app being restored has
+     * multiple certificates, and the backed up app also has identical multiple certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_sameMultCerts_restoresRuntimePermissions() {
+        install(APP_APK_CERT_1_2)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_1_2_DUP)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the app being restored has
+     * multiple certificates, and the backed up app do not have identical multiple certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_diffMultCerts_doesNotRestoreRuntimePermissions() {
+        install(APP_APK_CERT_1_2)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_3_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the app being restored has
+     * multiple certificates, and the backed up app's certificate is present in th restored app's
+     * certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_singleToMultiCert_restoresRuntimePerms() {
+        install(APP_APK_CERT_1)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_1_2_3)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the backed up app and the app
+     * being restored have multiple certificates, and the backed up app's certificates are a subset
+     * of the restored app's certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_multCertsToSuperset_doesNotRestoreRuntimePerms() {
+        install(APP_APK_CERT_1_2)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_1_2_3)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of regular runtime permissions, when the backed up app and the app
+     * being restored have multiple certificates, and the backed up app's certificates are a
+     * superset of the restored app's certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_multCertsToSubset_doesNotRestoreRuntimePermissions() {
+        install(APP_APK_CERT_1_2_3)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_1_2)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, READ_CONTACTS))
+        }
+    }
+
+    /**
+     * Test backup and restore of tri-state permissions, when both foreground and background runtime
+     * permissions are not granted and the backed up and restored app have compatible certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_fgBgDenied_matchingCerts_restoresFgBgPermissions() {
+        install(APP_APK_CERT_2)
+        if (!isBackupSupported) {
+            return
+        }
+        // Make a token change to permission state, to enable to us to determine when restore is
+        // complete.
+        grantPermission(APP, WRITE_CONTACTS)
+        // PERMISSION_DENIED is the default state, so we mark the permissions as user set in order
+        // to ensure that permissions are backed up.
+        setFlag(APP, ACCESS_FINE_LOCATION, FLAG_PERMISSION_USER_SET)
+        setFlag(APP, ACCESS_BACKGROUND_LOCATION, FLAG_PERMISSION_USER_SET)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+
+            // Wait until restore is complete.
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, WRITE_CONTACTS))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_BACKGROUND_LOCATION))
+            assertEquals(AppOpsManager.MODE_IGNORED, getAppOp(APP, ACCESS_FINE_LOCATION))
+        }
+    }
+
+    /**
+     * Test backup and restore of tri-state permissions, when both foreground and background runtime
+     * permissions are not granted and the backed up and restored app don't have compatible
+     * certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_fgBgDenied_notMatchingCerts_doesNotRestorePerms() {
+        install(APP_APK_CERT_1)
+        if (!isBackupSupported) {
+            return
+        }
+        // Make a token change to permission state, to enable to us to determine when restore is
+        // complete.
+        grantPermission(APP, WRITE_CONTACTS)
+        // PERMISSION_DENIED is the default state, so we mark the permissions as user set in order
+        // to ensure that permissions are backed up.
+        setFlag(APP, ACCESS_FINE_LOCATION, FLAG_PERMISSION_USER_SET)
+        setFlag(APP, ACCESS_BACKGROUND_LOCATION, FLAG_PERMISSION_USER_SET)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_2)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+
+            // Wait until restore is complete.
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, WRITE_CONTACTS))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_BACKGROUND_LOCATION))
+            assertEquals(AppOpsManager.MODE_IGNORED, getAppOp(APP, ACCESS_FINE_LOCATION))
+        }
+    }
+
+    /**
+     * Test backup and restore of tri-state permissions, when foreground runtime permission is
+     * granted and the backed up and restored app have compatible certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_fgGranted_matchingCerts_restoresFgBgPermissions() {
+        install(APP_APK_CERT_2)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+        // PERMISSION_DENIED is the default state, so we mark the permissions as user set in order
+        // to ensure that permissions are backed up.
+        setFlag(APP, ACCESS_BACKGROUND_LOCATION, FLAG_PERMISSION_USER_SET)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_BACKGROUND_LOCATION))
+            assertEquals(AppOpsManager.MODE_FOREGROUND, getAppOp(APP, ACCESS_FINE_LOCATION))
+        }
+    }
+
+    /**
+     * Test backup and restore of tri-state permissions, when foreground runtime permission is
+     * granted and the backed up and restored app don't have compatible certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_fgGranted_notMatchingCerts_doesNotRestoreFgBgPerms() {
+        install(APP_APK_CERT_1)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+        // PERMISSION_DENIED is the default state, so we mark the permissions as user set in order
+        // to ensure that permissions are backed up.
+        setFlag(APP, ACCESS_BACKGROUND_LOCATION, FLAG_PERMISSION_USER_SET)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_2)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_BACKGROUND_LOCATION))
+            assertEquals(AppOpsManager.MODE_IGNORED, getAppOp(APP, ACCESS_FINE_LOCATION))
+        }
+    }
+
+    /**
+     * Test backup and restore of tri-state permissions, when foreground and background runtime
+     * permissions are granted and the backed up and restored app have compatible certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_fgBgGranted_matchingCerts_restoresFgBgPermissions() {
+        install(APP_APK_CERT_2)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+        grantPermission(APP, ACCESS_BACKGROUND_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_BACKGROUND_LOCATION))
+            assertEquals(AppOpsManager.MODE_ALLOWED, getAppOp(APP, ACCESS_FINE_LOCATION))
+        }
+    }
+
+    /**
+     * Test backup and restore of tri-state permissions, when foreground and background runtime
+     * permissions are granted and the backed up and restored app don't have compatible
+     * certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_fgBgGranted_notMatchingCerts_restoresFgBgPerms() {
+        install(APP_APK_CERT_1)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+        grantPermission(APP, ACCESS_BACKGROUND_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_2)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually {
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION))
+            assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_BACKGROUND_LOCATION))
+            assertEquals(AppOpsManager.MODE_IGNORED, getAppOp(APP, ACCESS_FINE_LOCATION))
+        }
+    }
+
+    /**
+     * Test backup and restore of flags when the backed up app and restored app have compatible
+     * certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_matchingCerts_restoresFlags() {
+        install(APP_APK_CERT_2)
+        if (!isBackupSupported) {
+            return
+        }
+        setFlag(APP, WRITE_CONTACTS, FLAG_PERMISSION_USER_SET)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually { assertTrue(isFlagSet(APP, WRITE_CONTACTS, FLAG_PERMISSION_USER_SET)) }
+    }
+
+    /**
+     * Test backup and restore of flags when the backed up app and restored app don't have
+     * compatible certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_notMatchingCerts_doesNotRestoreFlag() {
+        install(APP_APK_CERT_1)
+        if (!isBackupSupported) {
+            return
+        }
+        setFlag(APP, WRITE_CONTACTS, FLAG_PERMISSION_USER_SET)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        install(APP_APK_CERT_2)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+
+        eventually { assertFalse(isFlagSet(APP, WRITE_CONTACTS, FLAG_PERMISSION_USER_SET)) }
+    }
+
+    /**
+     * Test backup and delayed restore of regular runtime permission, i.e. when an app is installed
+     * after restore has run, and the backed up app and restored app have compatible certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_appInstalledLater_matchingCerts_restoresCorrectly() {
+        install(APP_APK_CERT_2)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+
+        eventually { assertEquals(PERMISSION_GRANTED, checkPermission(APP, ACCESS_FINE_LOCATION)) }
+    }
+
+    /**
+     * Test backup and delayed restore of regular runtime permission, i.e. when an app is installed
+     * after restore has run, and the backed up app and restored app don't have compatible
+     * certificates.
+     */
+    @Test
+    @AsbSecurityTest(cveBugId = [184847040])
+    fun testRestore_appInstalledLater_notMatchingCerts_doesNotRestore() {
+        install(APP_APK_CERT_1)
+        if (!isBackupSupported) {
+            return
+        }
+        grantPermission(APP, ACCESS_FINE_LOCATION)
+
+        backupUtils.backupNowAndAssertSuccess(ANDROID_PACKAGE)
+        uninstallIfInstalled(APP)
+        backupUtils.restoreAndAssertSuccess(LOCAL_TRANSPORT_TOKEN, ANDROID_PACKAGE)
+        install(APP_APK_CERT_4_HISTORY_1_2_4)
+
+        eventually { assertEquals(PERMISSION_DENIED, checkPermission(APP, ACCESS_FINE_LOCATION)) }
+    }
+
+    private fun install(apk: String) {
+        val output = runShellCommand("pm install -r $apk")
+        assertEquals("Success", output)
+    }
+
+    private fun uninstallIfInstalled(packageName: String) {
+        runShellCommand("pm uninstall $packageName")
+    }
+
+    private fun setFlag(app: String, permission: String, flag: Int) {
+        runWithShellPermissionIdentity {
+            targetContext.packageManager.updatePermissionFlags(
+                permission, app, flag, flag, targetContext.user)
+        }
+    }
+
+    private fun clearFlag(app: String, permission: String, flag: Int) {
+        runWithShellPermissionIdentity {
+            targetContext.packageManager.updatePermissionFlags(
+                permission, app, flag, 0, targetContext.user)
+        }
+    }
+
+    private fun isFlagSet(app: String, permission: String, flag: Int): Boolean {
+        return try {
+            callWithShellPermissionIdentity<Int> {
+                targetContext.packageManager.getPermissionFlags(permission, app, targetContext.user)
+            } and flag == flag
+        } catch (e: Exception) {
+            throw RuntimeException(e)
+        }
+    }
+
+    private fun checkPermission(app: String, permission: String): Int {
+        return targetContext.packageManager.checkPermission(permission, app)
+    }
+
+    private fun getAppOp(app: String, permission: String): Int {
+        return try {
+            callWithShellPermissionIdentity {
+                targetContext
+                    .getSystemService<AppOpsManager>(AppOpsManager::class.java)!!
+                    .unsafeCheckOpRaw(
+                        AppOpsManager.permissionToOp(permission)!!,
+                        targetContext.packageManager.getPackageUid(app, 0),
+                        app)
+            }
+        } catch (e: Exception) {
+            throw RuntimeException(e)
+        }
+    }
+
+    companion object {
+        /** The name of the package of the apps under test */
+        private const val APP = "android.security.permissionbackup"
+        /** The apk of the packages */
+        private const val APK_PATH = "/data/local/tmp/cts/security/"
+        private const val APP_APK_CERT_1 = "${APK_PATH}CtsPermissionBackupAppCert1.apk"
+        private const val APP_APK_CERT_1_DUP = "${APK_PATH}CtsPermissionBackupAppCert1Dup.apk"
+        private const val APP_APK_CERT_2 = "${APK_PATH}CtsPermissionBackupAppCert2.apk"
+        private const val APP_APK_CERT_3 = "${APK_PATH}CtsPermissionBackupAppCert3.apk"
+        private const val APP_APK_CERT_4 = "${APK_PATH}CtsPermissionBackupAppCert4.apk"
+        private const val APP_APK_CERT_1_2 = "${APK_PATH}CtsPermissionBackupAppCert12.apk"
+        private const val APP_APK_CERT_1_2_DUP = "${APK_PATH}CtsPermissionBackupAppCert12Dup.apk"
+        private const val APP_APK_CERT_1_2_3 = "${APK_PATH}CtsPermissionBackupAppCert123.apk"
+        private const val APP_APK_CERT_3_4 = "${APK_PATH}CtsPermissionBackupAppCert34.apk"
+        private const val APP_APK_CERT_4_HISTORY_1_2_4 =
+            "${APK_PATH}CtsPermissionBackupAppCert4History124.apk"
+        private const val APP_LOG_TAG = "PermissionBackupApp"
+        /** The name of the package for backup */
+        private const val ANDROID_PACKAGE = "android"
+        private const val TIMEOUT_MILLIS: Long = 10000
+
+        /**
+         * Make sure that a [Runnable] eventually finishes without throwing an [Exception].
+         *
+         * @param r The [Runnable] to run.
+         */
+        fun eventually(r: Runnable) {
+            val start = System.currentTimeMillis()
+            while (true) {
+                try {
+                    r.run()
+                    return
+                } catch (e: Throwable) {
+                    if (System.currentTimeMillis() - start < TIMEOUT_MILLIS) {
+                        try {
+                            Thread.sleep(100)
+                        } catch (ignored: InterruptedException) {
+                            throw RuntimeException(e)
+                        }
+                    } else {
+                        throw e
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/tests/tests/security/test-cert-1.pk8 b/tests/tests/security/test-cert-1.pk8
new file mode 100644
index 0000000..f781c30
--- /dev/null
+++ b/tests/tests/security/test-cert-1.pk8
Binary files differ
diff --git a/tests/tests/security/test-cert-1.x509.pem b/tests/tests/security/test-cert-1.x509.pem
new file mode 100644
index 0000000..06adcfe
--- /dev/null
+++ b/tests/tests/security/test-cert-1.x509.pem
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----
+MIIBbDCCARGgAwIBAgIJAMoPtk37ZudyMAoGCCqGSM49BAMCMBIxEDAOBgNVBAMM
+B2VjLXAyNTYwHhcNMTYwMzMxMTQ1ODA2WhcNNDMwODE3MTQ1ODA2WjASMRAwDgYD
+VQQDDAdlYy1wMjU2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpl8RPSLLSROQ
+gwesMe4roOkTi3hfrGU20U6izpDStL/hlLUM3I4Wn1SnOpke8Pp2MpglvgeMx4J0
+BwPaRLTX66NQME4wHQYDVR0OBBYEFNQTNWi5WzAVizIgceqMQ/9bBczIMB8GA1Ud
+IwQYMBaAFNQTNWi5WzAVizIgceqMQ/9bBczIMAwGA1UdEwQFMAMBAf8wCgYIKoZI
+zj0EAwIDSQAwRgIhAPUEoIZsrvAp9BcULFy3E1THn/zR1kBhjfyk8Z4W23jWAiEA
++O6kgpeZwGytCMbT0tLsBeBXQVTnR+oP27gELLZVqt0=
+-----END CERTIFICATE-----
diff --git a/tests/tests/security/test-cert-2.pk8 b/tests/tests/security/test-cert-2.pk8
new file mode 100644
index 0000000..5e73f27
--- /dev/null
+++ b/tests/tests/security/test-cert-2.pk8
Binary files differ
diff --git a/tests/tests/security/test-cert-2.x509.pem b/tests/tests/security/test-cert-2.x509.pem
new file mode 100644
index 0000000..f8e5e65
--- /dev/null
+++ b/tests/tests/security/test-cert-2.x509.pem
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----
+MIIBbTCCAROgAwIBAgIJAIhVvR3SsrIlMAoGCCqGSM49BAMCMBIxEDAOBgNVBAMM
+B2VjLXAyNTYwHhcNMTgwNzEzMTc0MTUxWhcNMjgwNzEwMTc0MTUxWjAUMRIwEAYD
+VQQDDAllYy1wMjU2XzIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQdTMoEcq2X
+7jzs7w2pPWK0UMZ4gzOzbnVTzen3SrXfALu6a6lQ5oRh1wu8JxtiFR2tLeK/YgPN
+IHaAHHqdRCLho1AwTjAdBgNVHQ4EFgQUeZHZKwII/ESL9QbU78n/9CjLXl8wHwYD
+VR0jBBgwFoAU1BM1aLlbMBWLMiBx6oxD/1sFzMgwDAYDVR0TBAUwAwEB/zAKBggq
+hkjOPQQDAgNIADBFAiAnaauxtJ/C9TR5xK6SpmMdq/1SLJrLC7orQ+vrmcYwEQIh
+ANJg+x0fF2z5t/pgCYv9JDGfSQWj5f2hAKb+Giqxn/Ce
+-----END CERTIFICATE-----
diff --git a/tests/tests/security/test-cert-3.pk8 b/tests/tests/security/test-cert-3.pk8
new file mode 100644
index 0000000..d7309dd
--- /dev/null
+++ b/tests/tests/security/test-cert-3.pk8
Binary files differ
diff --git a/tests/tests/security/test-cert-3.x509.pem b/tests/tests/security/test-cert-3.x509.pem
new file mode 100644
index 0000000..c028ff7
--- /dev/null
+++ b/tests/tests/security/test-cert-3.x509.pem
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----
+MIIBbjCCARWgAwIBAgIJAIOU9crRaomnMAoGCCqGSM49BAMCMBQxEjAQBgNVBAMM
+CWVjLXAyNTZfMjAeFw0xODA3MTQwMDA1MjZaFw0yODA3MTEwMDA1MjZaMBQxEjAQ
+BgNVBAMMCWVjLXAyNTZfMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPMeYkMO
+nbb8WSjZdfxOR0GbrPyy4HyJKZ5s1+NE3SGt/TCNWMtJoaKj/srM7qSGIGnzC+Fk
+O8wlUEDYCJ37N0OjUDBOMB0GA1UdDgQWBBRvjQgosT769Xf8hrDpn6PlS8vP8DAf
+BgNVHSMEGDAWgBR5kdkrAgj8RIv1BtTvyf/0KMteXzAMBgNVHRMEBTADAQH/MAoG
+CCqGSM49BAMCA0cAMEQCICVr2qJ4TCc+TMKRpZWkZ3ne6d6QRNyferggMJVn35/p
+AiAaStjGmJG1qMR0NP6VQO0fSXm1+tNIPz+gTVZ3NVpXng==
+-----END CERTIFICATE-----
diff --git a/tests/tests/security/test-cert-4.pk8 b/tests/tests/security/test-cert-4.pk8
new file mode 100644
index 0000000..3675d50
--- /dev/null
+++ b/tests/tests/security/test-cert-4.pk8
Binary files differ
diff --git a/tests/tests/security/test-cert-4.x509.pem b/tests/tests/security/test-cert-4.x509.pem
new file mode 100644
index 0000000..4060400
--- /dev/null
+++ b/tests/tests/security/test-cert-4.x509.pem
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----
+MIIBezCCASCgAwIBAgIUbIy4qBhDPB5kMfsW+zrg+1rWCqcwCgYIKoZIzj0EAwIw
+FDESMBAGA1UEAwwJZWMtcDI1Nl8zMB4XDTIwMDUxMzE5MTUyOFoXDTMwMDUxMTE5
+MTUyOFowFDESMBAGA1UEAwwJZWMtcDI1Nl80MFkwEwYHKoZIzj0CAQYIKoZIzj0D
+AQcDQgAE20pgAx55rUnLdZAH1oVdRGm5HIurBlQ08vupca3n5NGVmaD2e15wjP2n
+VD5WMMN2nTfgk2QNfHaKFRRM0OXc9KNQME4wHQYDVR0OBBYEFG54lwMyVUM2tu6J
+JOqnAjDjk/Z4MB8GA1UdIwQYMBaAFG+NCCixPvr1d/yGsOmfo+VLy8/wMAwGA1Ud
+EwQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhAM54bnnsdUdEYILpyvkQYU/4B1j5
+gZ+w8UhpUGer4PzUAiEApIgeMy3ewhFq0rWc+JHQ8zH/fifne3xiBseYjZtTkzA=
+-----END CERTIFICATE-----
diff --git a/tests/tests/security/test-cert-with-1-2-4-in-rotation-history b/tests/tests/security/test-cert-with-1-2-4-in-rotation-history
new file mode 100644
index 0000000..7326e46
--- /dev/null
+++ b/tests/tests/security/test-cert-with-1-2-4-in-rotation-history
Binary files differ
diff --git a/tests/tests/security/testdata/permissionbackuptestapp/AndroidManifest.xml b/tests/tests/security/testdata/permissionbackuptestapp/AndroidManifest.xml
new file mode 100644
index 0000000..2b75d8c
--- /dev/null
+++ b/tests/tests/security/testdata/permissionbackuptestapp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.security.permissionbackup" >
+
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+
+    <application
+        android:label="Android Permission Backup CTS App">
+        <uses-library android:name="android.test.runner" />
+    </application>
+</manifest>
diff --git a/tests/tests/telecom/src/android/telecom/cts/OutgoingCallTest.java b/tests/tests/telecom/src/android/telecom/cts/OutgoingCallTest.java
index 62989a1..4c8b85a 100644
--- a/tests/tests/telecom/src/android/telecom/cts/OutgoingCallTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/OutgoingCallTest.java
@@ -31,7 +31,6 @@
 import android.telecom.Call;
 import android.telecom.CallAudioState;
 import android.telecom.Connection;
-import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
@@ -301,23 +300,23 @@
             return;
         }
 
+        mTelecomManager.registerPhoneAccount(TestUtils.TEST_PHONE_ACCOUNT);
+        TestUtils.enablePhoneAccount(getInstrumentation(), TestUtils.TEST_PHONE_ACCOUNT_HANDLE);
+        mTelecomManager.registerPhoneAccount(TestUtils.TEST_PHONE_ACCOUNT_2);
+        TestUtils.enablePhoneAccount(getInstrumentation(), TestUtils.TEST_PHONE_ACCOUNT_HANDLE_2);
+
         CountDownLatch latch = new CountDownLatch(1);
         mInCallCallbacks = new MockInCallService.InCallServiceCallbacks() {
             @Override
             public void onCallAdded(Call call, int numCalls) {
                 if (call.getState() == STATE_SELECT_PHONE_ACCOUNT) {
+                    call.phoneAccountSelected(TestUtils.TEST_PHONE_ACCOUNT_HANDLE, true);
                     latch.countDown();
                 }
             }
         };
         MockInCallService.setCallbacks(mInCallCallbacks);
 
-        mTelecomManager.registerPhoneAccount(TestUtils.TEST_PHONE_ACCOUNT);
-        TestUtils.enablePhoneAccount(getInstrumentation(), TestUtils.TEST_PHONE_ACCOUNT_HANDLE);
-        mTelecomManager.registerPhoneAccount(TestUtils.TEST_PHONE_ACCOUNT_2);
-        TestUtils.enablePhoneAccount(getInstrumentation(), TestUtils.TEST_PHONE_ACCOUNT_HANDLE_2);
-
-        PhoneAccountHandle cachedHandle = mTelecomManager.getUserSelectedOutgoingPhoneAccount();
         SystemUtil.runWithShellPermissionIdentity(() -> {
             mTelecomManager.setUserSelectedOutgoingPhoneAccount(null);
         });
@@ -327,10 +326,11 @@
             mTelecomManager.placeCall(testNumber, null);
 
             assertTrue(latch.await(TestUtils.WAIT_FOR_CALL_ADDED_TIMEOUT_S, TimeUnit.SECONDS));
+            assertEquals(TestUtils.TEST_PHONE_ACCOUNT_HANDLE,
+                    mTelecomManager.getUserSelectedOutgoingPhoneAccount());
         } finally {
-            SystemUtil.runWithShellPermissionIdentity(() -> {
-                mTelecomManager.setUserSelectedOutgoingPhoneAccount(cachedHandle);
-            });
+            mTelecomManager.unregisterPhoneAccount(TestUtils.TEST_PHONE_ACCOUNT_HANDLE);
+            mTelecomManager.unregisterPhoneAccount(TestUtils.TEST_PHONE_ACCOUNT_HANDLE_2);
         }
     }
 }
diff --git a/tests/tests/telecom/src/android/telecom/cts/PhoneAccountRegistrarTest.java b/tests/tests/telecom/src/android/telecom/cts/PhoneAccountRegistrarTest.java
index 80905dc..451d4e3 100644
--- a/tests/tests/telecom/src/android/telecom/cts/PhoneAccountRegistrarTest.java
+++ b/tests/tests/telecom/src/android/telecom/cts/PhoneAccountRegistrarTest.java
@@ -53,7 +53,7 @@
     // PhoneAccountRegistrar called MAX_PHONE_ACCOUNT_REGISTRATIONS
 
     // permissions
-    private static final String READ_PRIVILEGED_PHONE_STATE =
+    private static final String READ_PHONE_STATE_PERMISSION =
             "android.permission.READ_PRIVILEGED_PHONE_STATE";
     private static final String MODIFY_PHONE_STATE_PERMISSION =
             "android.permission.MODIFY_PHONE_STATE";
@@ -237,87 +237,7 @@
             }
         }
         // unbind from second package
-        mContext.unbindService(control);    
-    }
-
-    /*
-     * Ensure an app does not register accounts over the upper bound limit by disabling them
-     */
-    public void testDisablingAccountsAfterRegStillThrowsException() throws Exception {
-        if (!mShouldTestTelecom) return;
-
-        // ensure the test starts without any phone accounts registered to the test package
-        cleanupPhoneAccounts();
-
-        // Create MAX_PHONE_ACCOUNT_REGISTRATIONS + 1 via helper function
-        ArrayList<PhoneAccount> accounts = TestUtils.generateRandomPhoneAccounts(SEED,
-                MAX_PHONE_ACCOUNT_REGISTRATIONS + 1, TestUtils.PACKAGE,
-                TestUtils.COMPONENT);
-
-        try {
-            // Try to register more phone accounts than allowed by the upper bound limit
-            for (PhoneAccount pa : accounts) {
-                mTelecomManager.registerPhoneAccount(pa);
-                TestUtils.disablePhoneAccount(getInstrumentation(), pa.getAccountHandle());
-                // verify the account is both registered and disabled
-                verifyAccountIsDisabled(pa);
-            }
-
-            // A successful test should never reach this line of execution.
-            // However, if it does, fail the test by throwing a fail(...)
-            fail("Test failed. The test did not throw an IllegalArgumentException when "
-                    + "registering phone accounts over the upper bound: "
-                    + "MAX_PHONE_ACCOUNT_REGISTRATIONS");
-        } catch (IllegalArgumentException e) {
-            // Assert the IllegalArgumentException was thrown
-            assertNotNull(e.toString());
-        } finally {
-            // Cleanup accounts registered
-            accounts.stream().forEach(d -> mTelecomManager.unregisterPhoneAccount(
-                    d.getAccountHandle()));
-        }
-    }
-
-    /**
-     * Ensure an app does not register accounts that will be auto-disabled upon registered and
-     * bypass the limit. Note: CAPABILITY_CALL_PROVIDER will register the account as disabled.
-     */
-    public void testDisabledAccountsThrowsException() throws Exception {
-        if (!mShouldTestTelecom) return;
-
-        // ensure the test starts without any phone accounts registered to the test package
-        cleanupPhoneAccounts();
-
-        // Create MAX_PHONE_ACCOUNT_REGISTRATIONS + 1
-        ArrayList<PhoneAccount> accounts = new ArrayList<>();
-        for (int i = 0; i < MAX_PHONE_ACCOUNT_REGISTRATIONS + 1; i++) {
-            accounts.add(new PhoneAccount.Builder(
-                    TestUtils.makePhoneAccountHandle(Integer.toString(i)),
-                    TestUtils.ACCOUNT_LABEL)
-                    .setCapabilities(CAPABILITY_CALL_PROVIDER)
-                    .build());
-        }
-
-        try {
-            // Try to register more phone accounts than allowed by the upper bound limit
-            for (PhoneAccount pa : accounts) {
-                mTelecomManager.registerPhoneAccount(pa);
-                // verify the account is both registered and disabled
-                verifyAccountIsDisabled(pa);
-            }
-            // A successful test should never reach this line of execution.
-            // However, if it does, fail the test by throwing a fail(...)
-            fail("Test failed. The test did not throw an IllegalArgumentException when "
-                    + "registering phone accounts over the upper bound: "
-                    + "MAX_PHONE_ACCOUNT_REGISTRATIONS");
-        } catch (IllegalArgumentException e) {
-            // Assert the IllegalArgumentException was thrown
-            assertNotNull(e.toString());
-        } finally {
-            // Cleanup accounts registered
-            accounts.stream().forEach(d -> mTelecomManager.unregisterPhoneAccount(
-                    d.getAccountHandle()));
-        }
+        mContext.unbindService(control);
     }
 
     /**
@@ -581,12 +501,6 @@
         }
     }
 
-    public void verifyAccountIsDisabled(PhoneAccount account) {
-        PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(account.getAccountHandle());
-        assertNotNull(phoneAccount);
-        assertFalse(phoneAccount.isEnabled());
-    }
-
     public void verifyCanFetchCallCapableAccounts() {
         List<PhoneAccountHandle> res =
                 mTelecomManager.getCallCapablePhoneAccounts(true);
@@ -678,9 +592,10 @@
             if (mTelecomManager != null) {
                 // Get all handles registered to the testing package
                 List<PhoneAccountHandle> handles =
-                        ShellIdentityUtils.invokeMethodWithShellPermissions(mTelecomManager,
-                                (tm) -> tm.getPhoneAccountsForPackage(),
-                                READ_PRIVILEGED_PHONE_STATE);
+                        ShellIdentityUtils.invokeMethodWithShellPermissions(
+                                mTelecomManager, (tm) -> tm.getPhoneAccountsForPackage(),
+                                READ_PHONE_STATE_PERMISSION);
+
                 // cleanup any extra phone accounts registered to the testing package
                 if (handles.size() > 0 && mTelecomManager != null) {
                     handles.stream().forEach(
@@ -702,9 +617,9 @@
      */
     private int getNumberOfPhoneAccountsRegisteredToTestPackage() {
         if (mTelecomManager != null) {
-            return ShellIdentityUtils.invokeMethodWithShellPermissions(mTelecomManager,
-                    (tm) -> tm.getPhoneAccountsForPackage(),
-                    READ_PRIVILEGED_PHONE_STATE).size();
+            return ShellIdentityUtils.invokeMethodWithShellPermissions(
+                    mTelecomManager, (tm) -> tm.getPhoneAccountsForPackage(),
+                    READ_PHONE_STATE_PERMISSION).size();
         }
         return 0;
     }
diff --git a/tests/tests/telecom/src/android/telecom/cts/TestUtils.java b/tests/tests/telecom/src/android/telecom/cts/TestUtils.java
index 11e67cc..80fbe2d 100644
--- a/tests/tests/telecom/src/android/telecom/cts/TestUtils.java
+++ b/tests/tests/telecom/src/android/telecom/cts/TestUtils.java
@@ -81,8 +81,6 @@
     public static final String ACCOUNT_ID_SIM = "sim_acct";
     public static final String ACCOUNT_ID_EMERGENCY = "xtstest_CALL_PROVIDER_EMERGENCY";
     public static final String EXTRA_PHONE_NUMBER = "android.telecom.cts.extra.PHONE_NUMBER";
-    public static final ComponentName TELECOM_CTS_COMPONENT_NAME = new ComponentName(
-            TestUtils.PACKAGE, TestUtils.COMPONENT);
     public static final PhoneAccountHandle TEST_PHONE_ACCOUNT_HANDLE =
             new PhoneAccountHandle(new ComponentName(PACKAGE, COMPONENT), ACCOUNT_ID_1);
     public static final PhoneAccountHandle TEST_SIM_PHONE_ACCOUNT_HANDLE =
@@ -324,8 +322,6 @@
 
     private static final String COMMAND_ENABLE = "telecom set-phone-account-enabled ";
 
-    private static final String COMMAND_DISABLE = "telecom set-phone-account-disabled ";
-
     private static final String COMMAND_SET_ACCT_SUGGESTION =
             "telecom set-phone-acct-suggestion-component ";
 
@@ -412,15 +408,6 @@
                 + handle.getId() + " " + currentUserSerial);
     }
 
-    public static void disablePhoneAccount(Instrumentation instrumentation,
-            PhoneAccountHandle handle) throws Exception {
-        final ComponentName component = handle.getComponentName();
-        final long currentUserSerial = getCurrentUserSerialNumber(instrumentation);
-        executeShellCommand(instrumentation, COMMAND_DISABLE
-                + component.getPackageName() + "/" + component.getClassName() + " "
-                + handle.getId() + " " + currentUserSerial);
-    }
-
     public static void registerSimPhoneAccount(Instrumentation instrumentation,
             PhoneAccountHandle handle, String label, String address) throws Exception {
         final ComponentName component = handle.getComponentName();
@@ -879,7 +866,5 @@
         return UUID.nameUUIDFromBytes(array);
     }
 
-    public static PhoneAccountHandle makePhoneAccountHandle(String id) {
-        return new PhoneAccountHandle(TELECOM_CTS_COMPONENT_NAME, id);
-    }
+
 }
diff --git a/tests/tests/uidisolation/src/android/uidisolation/cts/PermissionTestService.java b/tests/tests/uidisolation/src/android/uidisolation/cts/PermissionTestService.java
index fb18c2d..3019148 100644
--- a/tests/tests/uidisolation/src/android/uidisolation/cts/PermissionTestService.java
+++ b/tests/tests/uidisolation/src/android/uidisolation/cts/PermissionTestService.java
@@ -26,13 +26,14 @@
 import android.os.RemoteException;
 import android.util.Log;
 import android.webkit.cts.CtsTestServer;
+import android.webkit.cts.SslMode;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.io.InputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
 
@@ -214,8 +215,7 @@
         CtsTestServer webServer = null;
         try {
             try {
-                webServer = new CtsTestServer(getApplication(),
-                        CtsTestServer.SslMode.TRUST_ANY_CLIENT);
+                webServer = new CtsTestServer(getApplication(), SslMode.TRUST_ANY_CLIENT);
             } catch (Exception e) {
                 Log.e(TAG, "Failed to create CtsTestServer.");
                 return false;
diff --git a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognitionServiceMicIndicatorTest.java b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognitionServiceMicIndicatorTest.java
index 3aab3fe..c91c089 100644
--- a/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognitionServiceMicIndicatorTest.java
+++ b/tests/tests/voiceRecognition/src/android/voicerecognition/cts/RecognitionServiceMicIndicatorTest.java
@@ -209,10 +209,6 @@
 
     @Test
     public void testTrustedRecognitionServiceCanBlameCallingApp() throws Throwable {
-        // skip test for automototive devices, as Mic indicator is not a MUST requirement as per CDD
-        assumeFalse(mContext.getPackageManager()
-            .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE));
-
         // We treat trusted if the current voice recognizer is also a preinstalled app. This is a
         // trusted case.
         boolean hasPreInstalledRecognizer = hasPreInstalledRecognizer(
diff --git a/tests/tests/webkit/Android.bp b/tests/tests/webkit/Android.bp
index d8175dd..0b206ff 100644
--- a/tests/tests/webkit/Android.bp
+++ b/tests/tests/webkit/Android.bp
@@ -16,25 +16,41 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-android_test {
-    name: "CtsWebkitTestCases",
+android_library {
+    name: "CtsWebkitTestCasesSharedWithSdk",
     defaults: ["cts_defaults"],
+    manifest: "shared/AndroidManifest.xml",
+    resource_dirs: ["shared/res"],
     libs: [
         "android.test.runner",
         "org.apache.http.legacy",
         "android.test.base",
     ],
     static_libs: [
+        "androidx.test.core",
         "compatibility-device-util-axt",
         "ctsdeviceutillegacy-axt",
         "ctstestserver",
         "ctstestrunner-axt",
         "hamcrest-library",
+        "ctswebkitsharedenv",
     ],
     srcs: [
         "src/**/*.java",
         "src/**/*.aidl",
     ],
+    // uncomment when dalvik.annotation.Test* are removed or part of SDK
+    //sdk_version: "current"
+    platform_apis: true,
+}
+
+android_test {
+    name: "CtsWebkitTestCases",
+    defaults: ["cts_defaults"],
+    resource_dirs: ["res"],
+    static_libs: [
+        "CtsWebkitTestCasesSharedWithSdk",
+    ],
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
diff --git a/tests/uwb/AndroidManifest.xml b/tests/tests/webkit/shared/AndroidManifest.xml
similarity index 66%
rename from tests/uwb/AndroidManifest.xml
rename to tests/tests/webkit/shared/AndroidManifest.xml
index adecade..fc3e5cd 100644
--- a/tests/uwb/AndroidManifest.xml
+++ b/tests/tests/webkit/shared/AndroidManifest.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -16,13 +16,5 @@
  -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="android.uwb.cts">
-
-    <!--  self-instrumenting test package. -->
-    <instrumentation
-        android:name="androidx.test.runner.AndroidJUnitRunner"
-        android:label="CTS tests for android.uwb"
-        android:targetPackage="android.uwb.cts" >
-    </instrumentation>
+     package="android.webkit.cts">
 </manifest>
-
diff --git a/tests/tests/webkit/shared/README.md b/tests/tests/webkit/shared/README.md
new file mode 100644
index 0000000..73054f4
--- /dev/null
+++ b/tests/tests/webkit/shared/README.md
@@ -0,0 +1,7 @@
+# Shared with SDK resources
+
+This directory contains any resources that
+should be shared with the WebView SDK Runtime tests.
+
+It also contains the AndroidManifest.xml that will be used
+to create a shared library.
\ No newline at end of file
diff --git a/tests/tests/webkit/res/layout/webview_layout.xml b/tests/tests/webkit/shared/res/layout/webview_layout.xml
similarity index 100%
rename from tests/tests/webkit/res/layout/webview_layout.xml
rename to tests/tests/webkit/shared/res/layout/webview_layout.xml
diff --git a/tests/tests/webkit/res/raw/trustedcert.crt b/tests/tests/webkit/shared/res/raw/trustedcert.crt
similarity index 100%
rename from tests/tests/webkit/res/raw/trustedcert.crt
rename to tests/tests/webkit/shared/res/raw/trustedcert.crt
diff --git a/tests/tests/webkit/res/raw/trustedkey.der b/tests/tests/webkit/shared/res/raw/trustedkey.der
similarity index 100%
rename from tests/tests/webkit/res/raw/trustedkey.der
rename to tests/tests/webkit/shared/res/raw/trustedkey.der
Binary files differ
diff --git a/tests/tests/webkit/res/raw/untrustedcert.crt b/tests/tests/webkit/shared/res/raw/untrustedcert.crt
similarity index 100%
rename from tests/tests/webkit/res/raw/untrustedcert.crt
rename to tests/tests/webkit/shared/res/raw/untrustedcert.crt
diff --git a/tests/tests/webkit/res/raw/untrustedkey.der b/tests/tests/webkit/shared/res/raw/untrustedkey.der
similarity index 100%
rename from tests/tests/webkit/res/raw/untrustedkey.der
rename to tests/tests/webkit/shared/res/raw/untrustedkey.der
Binary files differ
diff --git a/tests/tests/webkit/res/xml/network_security_config.xml b/tests/tests/webkit/shared/res/xml/network_security_config.xml
similarity index 100%
rename from tests/tests/webkit/res/xml/network_security_config.xml
rename to tests/tests/webkit/shared/res/xml/network_security_config.xml
diff --git a/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java b/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java
index 4e390c9..0c63294 100644
--- a/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/CookieManagerTest.java
@@ -16,70 +16,107 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.CookieManager;
-import android.webkit.CookieSyncManager;
-import android.webkit.WebView;
 import android.webkit.ValueCallback;
+import android.webkit.WebView;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Date;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
-
-import java.util.Date;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 @AppModeFull
-public class CookieManagerTest extends
-        ActivityInstrumentationTestCase2<CookieSyncManagerCtsActivity> {
-
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CookieManagerTest extends SharedWebViewTest {
     private static final int TEST_TIMEOUT = 5000;
 
-    private WebView mWebView;
     private CookieManager mCookieManager;
     private WebViewOnUiThread mOnUiThread;
-    private CtsTestServer mServer;
+    private SharedSdkWebServer mWebServer;
 
-    public CookieManagerTest() {
-        super("android.webkit.cts", CookieSyncManagerCtsActivity.class);
-    }
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(CookieSyncManagerCtsActivity.class);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mWebView = getActivity().getWebView();
-        if (mWebView != null) {
-            mOnUiThread = new WebViewOnUiThread(mWebView);
-
-            mCookieManager = CookieManager.getInstance();
-            assertNotNull(mCookieManager);
-
-            // We start with no cookies.
-            mCookieManager.removeAllCookie();
-            assertFalse(mCookieManager.hasCookies());
-
-            // But accepting cookies.
-            mCookieManager.setAcceptCookie(false);
-            assertFalse(mCookieManager.acceptCookie());
-        }
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        if (mServer != null) {
-            mServer.shutdown();
-        }
-    }
-
-    public void testGetInstance() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
+    @Before
+    public void setUp() throws Exception {
+        WebView webView = getTestEnvironment().getWebView();
+        if (webView == null) {
             return;
         }
+
+        mOnUiThread = new WebViewOnUiThread(webView);
+
+        mCookieManager = CookieManager.getInstance();
+        assertNotNull(mCookieManager);
+
+        // We start with no cookies.
+        mCookieManager.removeAllCookie();
+        assertFalse(mCookieManager.hasCookies());
+
+        // But accepting cookies.
+        mCookieManager.setAcceptCookie(false);
+        assertFalse(mCookieManager.acceptCookie());
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((CookieSyncManagerCtsActivity) activity)
+                                        .getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                activity))
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mWebServer != null) {
+            mWebServer.shutdown();
+        }
+        if (mOnUiThread != null) {
+            mOnUiThread.cleanUp();
+        }
+    }
+
+    @Test
+    public void testGetInstance() {
         mOnUiThread.cleanUp();
         CookieManager c1 = CookieManager.getInstance();
         CookieManager c2 = CookieManager.getInstance();
@@ -87,29 +124,18 @@
         assertSame(c1, c2);
     }
 
-    public void testClone() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-    }
-
+    @Test
     public void testFlush() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mCookieManager.flush();
     }
 
+    @Test
     public void testAcceptCookie() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mCookieManager.setAcceptCookie(false);
         assertFalse(mCookieManager.acceptCookie());
 
-        mServer = new CtsTestServer(getActivity(), false);
-        String url = mServer.getCookieUrl("conquest.html");
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
+        String url = mWebServer.getCookieUrl("conquest.html");
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals("0", mOnUiThread.getTitle()); // no cookies passed
         Thread.sleep(500);
@@ -118,7 +144,7 @@
         mCookieManager.setAcceptCookie(true);
         assertTrue(mCookieManager.acceptCookie());
 
-        url = mServer.getCookieUrl("war.html");
+        url = mWebServer.getCookieUrl("war.html");
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals("0", mOnUiThread.getTitle()); // no cookies passed
         waitForCookie(url);
@@ -130,7 +156,7 @@
         assertTrue(m.matches());
         assertEquals("0", m.group(1));
 
-        url = mServer.getCookieUrl("famine.html");
+        url = mWebServer.getCookieUrl("famine.html");
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals("1|count=0", mOnUiThread.getTitle()); // outgoing cookie
         waitForCookie(url);
@@ -140,7 +166,7 @@
         assertTrue(m.matches());
         assertEquals("1", m.group(1)); // value got incremented
 
-        url = mServer.getCookieUrl("death.html");
+        url = mWebServer.getCookieUrl("death.html");
         mCookieManager.setCookie(url, "count=41");
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals("1|count=41", mOnUiThread.getTitle()); // outgoing cookie
@@ -152,11 +178,8 @@
         assertEquals("42", m.group(1)); // value got incremented
     }
 
+    @Test
     public void testSetCookie() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         String url = "http://www.example.com";
         String cookie = "name=test";
         mCookieManager.setCookie(url, cookie);
@@ -164,11 +187,8 @@
         assertTrue(mCookieManager.hasCookies());
     }
 
+    @Test
     public void testSetCookieNullCallback() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final String url = "http://www.example.com";
         final String cookie = "name=test";
         mCookieManager.setCookie(url, cookie, null);
@@ -181,11 +201,8 @@
         }.run();
     }
 
+    @Test
     public void testSetCookieCallback() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final Semaphore s = new Semaphore(0);
         final AtomicBoolean status = new AtomicBoolean();
         final ValueCallback<Boolean> callback = new ValueCallback<Boolean>() {
@@ -197,11 +214,8 @@
         };
     }
 
+    @Test
     public void testRemoveCookies() throws InterruptedException {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final String url = "http://www.example.com";
         final String sessionCookie = "cookie1=peter";
         final String longCookie = "cookie2=sue";
@@ -234,11 +248,8 @@
         assertFalse(mCookieManager.hasCookies());
     }
 
+    @Test
     public void testRemoveCookiesNullCallback() throws InterruptedException {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final String url = "http://www.example.com";
         final String sessionCookie = "cookie1=peter";
         final String longCookie = "cookie2=sue";
@@ -275,11 +286,8 @@
         assertNull(mCookieManager.getCookie(url));
     }
 
+    @Test
     public void testRemoveCookiesCallback() throws InterruptedException {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final Semaphore s = new Semaphore(0);
         final AtomicBoolean anyDeleted = new AtomicBoolean();
         final ValueCallback<Boolean> callback = new ValueCallback<Boolean>() {
@@ -329,11 +337,8 @@
         assertFalse(anyDeleted.get());
     }
 
+    @Test
     public void testThirdPartyCookie() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         // In theory we need two servers to test this, one server ('the first party')
         // which returns a response with a link to a second server ('the third party')
         // at different origin. This second server attempts to set a cookie which should
@@ -344,7 +349,7 @@
         // port. Instead we cheat making some of the urls come from localhost and some
         // from 127.0.0.1 which count (both in theory and pratice) as having different
         // origins.
-        mServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
         // Turn on Javascript (otherwise <script> aren't fetched spoiling the test).
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
@@ -360,9 +365,10 @@
         // ...we can't set third party cookies.
         // First on the third party server we get a url which tries to set a cookie.
         String cookieUrl = toThirdPartyUrl(
-                mServer.getSetCookieUrl("/cookie_1.js", "test1", "value1", "SameSite=None; Secure"));
+                mWebServer.getSetCookieUrl("/cookie_1.js", "test1", "value1",
+                        "SameSite=None; Secure"));
         // Then we create a url on the first party server which links to the first url.
-        String url = mServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
+        String url = mWebServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertNull(mCookieManager.getCookie(cookieUrl));
 
@@ -372,8 +378,8 @@
 
         // ...we can set third party cookies.
         cookieUrl = toThirdPartyUrl(
-                mServer.getSetCookieUrl("/cookie_2.js", "test2", "value2", "SameSite=None; Secure"));
-        url = mServer.getLinkedScriptUrl("/content_2.html", cookieUrl);
+            mWebServer.getSetCookieUrl("/cookie_2.js", "test2", "value2", "SameSite=None; Secure"));
+        url = mWebServer.getLinkedScriptUrl("/content_2.html", cookieUrl);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         waitForCookie(cookieUrl);
         String cookie = mCookieManager.getCookie(cookieUrl);
@@ -381,11 +387,9 @@
         assertTrue(cookie.contains("test2"));
     }
 
+    @Test
     public void testSameSiteLaxByDefault() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         mCookieManager.setAcceptCookie(true);
         mOnUiThread.setAcceptThirdPartyCookies(true);
@@ -393,44 +397,42 @@
         // Verify that even with third party cookies enabled, cookies that don't explicitly
         // specify SameSite=none are treated as SameSite=lax and not set in a 3P context.
         String cookieUrl = toThirdPartyUrl(
-                mServer.getSetCookieUrl("/cookie_1.js", "test1", "value1"));
-        String url = mServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
+                mWebServer.getSetCookieUrl("/cookie_1.js", "test1", "value1", null));
+        String url = mWebServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertNull(mCookieManager.getCookie(cookieUrl));
     }
 
+    @Test
     public void testSameSiteNoneRequiresSecure() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         mCookieManager.setAcceptCookie(true);
 
         // Verify that cookies with SameSite=none are ignored when the cookie is not also Secure.
         String cookieUrl =
-                mServer.getSetCookieUrl("/cookie_1.js", "test1", "value1", "SameSite=None");
-        String url = mServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
+                mWebServer.getSetCookieUrl("/cookie_1.js", "test1", "value1", "SameSite=None");
+        String url = mWebServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertNull(mCookieManager.getCookie(cookieUrl));
     }
 
+    @Test
     public void testSchemefulSameSite() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         mCookieManager.setAcceptCookie(true);
         mOnUiThread.setAcceptThirdPartyCookies(true);
 
         // Verify that two servers with different schemes on the same host are not considered
         // same-site to each other.
-        CtsTestServer secureServer = new CtsTestServer(getActivity(),
-                CtsTestServer.SslMode.NO_CLIENT_AUTH, R.raw.trustedkey, R.raw.trustedcert);
+        SharedSdkWebServer secureServer = getTestEnvironment()
+                .getSetupWebServer(SslMode.NO_CLIENT_AUTH, null,
+                        R.raw.trustedkey, R.raw.trustedcert);
         try {
-            String cookieUrl = secureServer.getSetCookieUrl("/cookie_1.js", "test1", "value1");
-            String url = mServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
+            String cookieUrl = secureServer.getSetCookieUrl("/cookie_1.js", "test1",
+                    "value1", null);
+            String url = mWebServer.getLinkedScriptUrl("/content_1.html", cookieUrl);
             mOnUiThread.loadUrlAndWaitForCompletion(url);
             assertNull(mCookieManager.getCookie(cookieUrl));
         } finally {
@@ -438,10 +440,8 @@
         }
     }
 
+    @Test
     public void testb3167208() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         String uri = "http://host.android.com/path/";
         // note the space after the domain=
         String problemCookie = "foo=bar; domain= .android.com; path=/";
diff --git a/tests/tests/webkit/src/android/webkit/cts/CookieTest.java b/tests/tests/webkit/src/android/webkit/cts/CookieTest.java
index 11a4482..a85c84e 100644
--- a/tests/tests/webkit/src/android/webkit/cts/CookieTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/CookieTest.java
@@ -17,29 +17,44 @@
 package android.webkit.cts;
 
 import android.platform.test.annotations.Presubmit;
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.CookieManager;
-import android.webkit.CookieSyncManager;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.webkit.WebView;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 /**
  * Original framework tests for CookieManager
  */
-public class CookieTest extends ActivityInstrumentationTestCase2<CookieSyncManagerCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class CookieTest extends SharedWebViewTest {
 
     private CookieManager mCookieManager;
     private static final long WAIT_TIME = 50;
 
-    public CookieTest() {
-        super("android.webkit.cts", CookieSyncManagerCtsActivity.class);
-    }
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(CookieSyncManagerCtsActivity.class);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        if (getActivity().getWebView() == null) {
+    @Before
+    public void setUp() throws Exception {
+        if (getTestEnvironment().getWebView() == null) {
             return;
         }
 
@@ -58,11 +73,30 @@
         assertFalse(mCookieManager.hasCookies());
     }
 
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((CookieSyncManagerCtsActivity) activity)
+                                        .getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                activity))
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
+    }
+
     @Presubmit
+    @Test
     public void testDomain() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         String url = "http://www.foo.com";
 
         // basic
@@ -100,10 +134,8 @@
         assertEquals("c=d", cookie);
     }
 
+    @Test
     public void testSubDomain() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         String url_abcd = "http://a.b.c.d.com";
         String url_bcd = "http://b.c.d.com";
         String url_cd = "http://c.d.com";
@@ -144,10 +176,8 @@
         assertTrue(cookie.contains("x=cd"));
     }
 
+    @Test
     public void testInvalidDomain() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         String url = "http://foo.bar.com";
 
         mCookieManager.setCookie(url, "a=1; domain=.yo.foo.bar.com");
@@ -183,10 +213,8 @@
         assertNull(cookie);
     }
 
+    @Test
     public void testPath() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         String url = "http://www.foo.com";
 
         mCookieManager.setCookie(url, "a=b; path=/wee");
@@ -215,10 +243,8 @@
         assertEquals("a=b; a=d", cookie);
     }
 
+    @Test
     public void testEmptyValue() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         String url = "http://www.foobar.com";
 
         mCookieManager.setCookie(url, "bar=");
diff --git a/tests/tests/webkit/src/android/webkit/cts/DateSorterTest.java b/tests/tests/webkit/src/android/webkit/cts/DateSorterTest.java
index 6de8d848..5aec07b 100644
--- a/tests/tests/webkit/src/android/webkit/cts/DateSorterTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/DateSorterTest.java
@@ -19,27 +19,48 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 
 import android.content.Context;
-import android.test.AndroidTestCase;
 import android.webkit.DateSorter;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.HashSet;
 
-public class DateSorterTest extends AndroidTestCase {
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DateSorterTest extends SharedWebViewTest {
     private Context mContext;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mContext = getContext();
+    @Before
+    public void setUp() throws Exception {
+        mContext = getTestEnvironment().getContext();
     }
 
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        return new SharedWebViewTestEnvironment.Builder()
+            .setContext(context)
+            .setHostAppInvoker(SharedWebViewTestEnvironment.createHostAppInvoker(context))
+            .build();
+    }
+
+    @Test
     public void testConstructor() {
         new DateSorter(mContext);
     }
 
+    @Test
     public void testConstants() {
         // according to DateSorter javadoc
         assertThat("DAY_COUNT should be >= 3",
@@ -47,6 +68,7 @@
                 greaterThanOrEqualTo(3));
     }
 
+    @Test
     public void testGetLabel() {
         DateSorter dateSorter = new DateSorter(mContext);
         HashSet<String> set = new HashSet<String>();
@@ -61,6 +83,7 @@
         }
     }
 
+    @Test
     public void testGetIndex() {
         DateSorter dateSorter = new DateSorter(mContext);
 
@@ -75,6 +98,7 @@
         }
     }
 
+    @Test
     public void testGetBoundary() {
         DateSorter dateSorter = new DateSorter(mContext);
 
diff --git a/tests/tests/webkit/src/android/webkit/cts/GeolocationTest.java b/tests/tests/webkit/src/android/webkit/cts/GeolocationTest.java
index 1087984..6f5e506 100644
--- a/tests/tests/webkit/src/android/webkit/cts/GeolocationTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/GeolocationTest.java
@@ -16,49 +16,50 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertFalse;
+
 import android.content.Context;
-import android.graphics.Bitmap;
 import android.location.Criteria;
 import android.location.Location;
-import android.location.LocationListener;
 import android.location.LocationManager;
 import android.location.LocationProvider;
-import android.platform.test.annotations.AppModeFull;
-import android.os.Bundle;
-import android.os.Looper;
 import android.os.SystemClock;
-import android.test.ActivityInstrumentationTestCase2;
-import android.webkit.CookieManager;
-import android.webkit.CookieSyncManager;
+import android.platform.test.annotations.AppModeFull;
 import android.webkit.GeolocationPermissions;
 import android.webkit.JavascriptInterface;
-import android.webkit.WebChromeClient;
 import android.webkit.WebResourceResponse;
 import android.webkit.WebView;
-import android.webkit.WebViewClient;
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
 import android.webkit.cts.WebViewSyncLoader.WaitForProgressClient;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.compatibility.common.util.LocationUtils;
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.ByteArrayInputStream;
 import java.io.UnsupportedEncodingException;
-import java.util.concurrent.Callable;
-import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Random;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 import java.util.Set;
 import java.util.TreeSet;
-
-import junit.framework.Assert;
+import java.util.concurrent.Callable;
 
 @AppModeFull(reason = "Instant apps do not have access to location information")
-public class GeolocationTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class GeolocationTest {
 
     // TODO Write additional tests to cover:
     // - test that the errors are correct
@@ -107,6 +108,10 @@
             "  </body>\n" +
             "</html>";
 
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
     private JavascriptStatusReceiver mJavascriptStatusReceiver;
     private LocationManager mLocationManager;
     private WebViewOnUiThread mOnUiThread;
@@ -114,10 +119,6 @@
     private volatile boolean mLocationUpdateThreadExitRequested;
     private List<String> mProviders;
 
-    public GeolocationTest() throws Exception {
-        super("android.webkit.cts", WebViewCtsActivity.class);
-    }
-
     // Both this test and WebViewOnUiThread need to override some of the methods on WebViewClient,
     // so this test sublclasses the WebViewClient from WebViewOnUiThread
     private static class InterceptClient extends WaitForLoadedClient {
@@ -132,27 +133,33 @@
             try {
                 return new WebResourceResponse("text/html", "utf-8",
                     new ByteArrayInputStream(RAW_HTML.getBytes("UTF-8")));
-            } catch(java.io.UnsupportedEncodingException e) {
+            } catch (UnsupportedEncodingException e) {
                 return null;
             }
         }
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setUp() throws Exception {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            WebViewCtsActivity webViewCtsActivity = (WebViewCtsActivity) activity;
+            WebView webview = webViewCtsActivity.getWebView();
+            if (webview != null) {
+                mOnUiThread = new WebViewOnUiThread(webview);
+            }
+        });
+        LocationUtils.registerMockLocationProvider(
+                InstrumentationRegistry.getInstrumentation(), true);
 
-        LocationUtils.registerMockLocationProvider(getInstrumentation(), true);
-        WebView webview = getActivity().getWebView();
-
-        if (webview != null) {
+        if (mOnUiThread != null) {
             // Set up a WebView with JavaScript and Geolocation enabled
             final String GEO_DIR = "geo_test";
-            mOnUiThread = new WebViewOnUiThread(webview);
             mOnUiThread.getSettings().setJavaScriptEnabled(true);
             mOnUiThread.getSettings().setGeolocationEnabled(true);
             mOnUiThread.getSettings().setGeolocationDatabasePath(
-                    getActivity().getApplicationContext().getDir(GEO_DIR, 0).getPath());
+                    InstrumentationRegistry.getInstrumentation().getContext().getDir(GEO_DIR, 0)
+                    .getPath());
 
             // Add a JsInterface to report back to the test when a location is received
             mJavascriptStatusReceiver = new JavascriptStatusReceiver();
@@ -163,8 +170,8 @@
             // Clear all permissions before each test
             GeolocationPermissions.getInstance().clearAll();
             // Cache this mostly because the lookup is two lines of code
-            mLocationManager = (LocationManager)getActivity().getApplicationContext()
-                    .getSystemService(Context.LOCATION_SERVICE);
+            mLocationManager = (LocationManager) InstrumentationRegistry.getInstrumentation()
+                    .getContext().getSystemService(Context.LOCATION_SERVICE);
             // Add a test provider before each test to inject a location
             mProviders = mLocationManager.getAllProviders();
             for (String provider : mProviders) {
@@ -184,8 +191,8 @@
         }
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         stopUpdateLocationThread();
         if (mProviders != null) {
             // Remove the test provider after each test
@@ -201,13 +208,12 @@
                 removeTestLocationProvider();
             }
         }
-        LocationUtils.registerMockLocationProvider(getInstrumentation(), false);
+        LocationUtils.registerMockLocationProvider(
+                InstrumentationRegistry.getInstrumentation(), false);
 
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-        // This will null all member and static variables
-        super.tearDown();
     }
 
     private void addTestProviders() {
@@ -266,7 +272,7 @@
                     while (!mLocationUpdateThreadExitRequested) {
                         try {
                             Thread.sleep(LOCATION_THREAD_UPDATE_WAIT_MS);
-                        } catch(Exception e) {
+                        } catch (Exception e) {
                             // Do nothing, an extra update is no problem
                         }
                         updateLocation();
@@ -334,10 +340,8 @@
     }
 
     // Test loading a page and accepting the domain for one load
+    @Test
     public void testSimpleGeolocationRequestAcceptOnce() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final TestSimpleGeolocationRequestWebChromeClient chromeClientAcceptOnce =
                 new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, true, false);
         mOnUiThread.setWebChromeClient(chromeClientAcceptOnce);
@@ -427,10 +431,8 @@
     }
 
     // Test loading a page and retaining the domain forever
+    @Test
     public void testSimpleGeolocationRequestAcceptAlways() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final TestSimpleGeolocationRequestWebChromeClient chromeClientAcceptAlways =
                 new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, true, true);
         mOnUiThread.setWebChromeClient(chromeClientAcceptAlways);
@@ -486,10 +488,8 @@
     }
 
     // Test the GeolocationPermissions API
+    @Test
     public void testGeolocationPermissions() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         Set<String> acceptedOrigins = new TreeSet<String>();
         BooleanCheck falseCheck = new BooleanCheck(false);
         GeolocationPermissions.getInstance().getAllowed(URL_2, falseCheck);
@@ -548,10 +548,8 @@
     }
 
     // Test loading pages and checks rejecting once and rejecting the domain forever
+    @Test
     public void testSimpleGeolocationRequestReject() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final TestSimpleGeolocationRequestWebChromeClient chromeClientRejectOnce =
                 new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, false, false);
         mOnUiThread.setWebChromeClient(chromeClientRejectOnce);
@@ -603,10 +601,8 @@
     }
 
     // Test deny geolocation on insecure origins
+    @Test
     public void testGeolocationRequestDeniedOnInsecureOrigin() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final TestSimpleGeolocationRequestWebChromeClient chromeClientAcceptAlways =
                 new TestSimpleGeolocationRequestWebChromeClient(mOnUiThread, true, true);
         mOnUiThread.setWebChromeClient(chromeClientAcceptAlways);
diff --git a/tests/tests/webkit/src/android/webkit/cts/HttpAuthHandlerTest.java b/tests/tests/webkit/src/android/webkit/cts/HttpAuthHandlerTest.java
index 2b12f19..6dcf019 100644
--- a/tests/tests/webkit/src/android/webkit/cts/HttpAuthHandlerTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/HttpAuthHandlerTest.java
@@ -16,19 +16,33 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.HttpAuthHandler;
 import android.webkit.WebView;
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 
 import org.apache.http.HttpStatus;
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 @AppModeFull
-public class HttpAuthHandlerTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
-
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class HttpAuthHandlerTest extends SharedWebViewTest {
     private static final long TIMEOUT = 10000;
 
     private static final String WRONG_USERNAME = "wrong_user";
@@ -36,32 +50,52 @@
     private static final String CORRECT_USERNAME = CtsTestServer.AUTH_USER;
     private static final String CORRECT_PASSWORD = CtsTestServer.AUTH_PASS;
 
-    private CtsTestServer mWebServer;
+    private SharedSdkWebServer mWebServer;
     private WebViewOnUiThread mOnUiThread;
 
-    public HttpAuthHandlerTest() {
-        super("android.webkit.cts", WebViewCtsActivity.class);
-    }
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        WebView webview = getActivity().getWebView();
+    @Before
+    public void setUp() throws Exception {
+        WebView webview = getTestEnvironment().getWebView();
         if (webview != null) {
             mOnUiThread = new WebViewOnUiThread(webview);
         }
+
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-
         if (mWebServer != null) {
             mWebServer.shutdown();
         }
-        super.tearDown();
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                activity))
+                                    .setContext(activity)
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
     }
 
     private class ProceedHttpAuthClient extends WaitForLoadedClient {
@@ -146,11 +180,8 @@
         assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testProceed() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity());
         String url = mWebServer.getAuthAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
 
         incorrectCredentialsAccessDenied(url);
@@ -158,11 +189,8 @@
         correctCredentialsAccessGranted(url);
     }
 
+    @Test
     public void testCancel() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity());
         String url = mWebServer.getAuthAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
 
         CancelHttpAuthClient client = new CancelHttpAuthClient();
@@ -173,11 +201,8 @@
         assertEquals(CtsTestServer.getReasonString(HttpStatus.SC_UNAUTHORIZED), mOnUiThread.getTitle());
     }
 
+    @Test
     public void testUseHttpAuthUsernamePassword() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity());
         String url = mWebServer.getAuthAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
 
         // Try to login once with incorrect credentials. This should cause
diff --git a/tests/tests/webkit/src/android/webkit/cts/MimeTypeMapTest.java b/tests/tests/webkit/src/android/webkit/cts/MimeTypeMapTest.java
index 6c0ff78..b59d19c 100644
--- a/tests/tests/webkit/src/android/webkit/cts/MimeTypeMapTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/MimeTypeMapTest.java
@@ -16,21 +16,38 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
 
-import android.test.AndroidTestCase;
 import android.webkit.MimeTypeMap;
 
-public class MimeTypeMapTest extends AndroidTestCase {
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MimeTypeMapTest extends SharedWebViewTest{
 
     private MimeTypeMap mMimeTypeMap;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
+    @Before
+    public void setUp() throws Exception {
         mMimeTypeMap = MimeTypeMap.getSingleton();
     }
 
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        return new SharedWebViewTestEnvironment.Builder().build();
+    }
+
+    @Test
     public void testGetFileExtensionFromUrl() {
         assertEquals("html", MimeTypeMap.getFileExtensionFromUrl("http://localhost/index.html"));
         assertEquals("html", MimeTypeMap.getFileExtensionFromUrl("http://host/x.html?x=y"));
@@ -42,8 +59,9 @@
 
         // ToBeFixed: Uncomment the following line after fixing the implementation
         //assertEquals("", MimeTypeMap.getFileExtensionFromUrl("http://www.example.com"));
-}
+    }
 
+    @Test
     public void testHasMimeType() {
         assertTrue(mMimeTypeMap.hasMimeType("audio/mpeg"));
         assertTrue(mMimeTypeMap.hasMimeType("text/plain"));
@@ -54,6 +72,7 @@
         assertFalse(mMimeTypeMap.hasMimeType(null));
     }
 
+    @Test
     public void testGetMimeTypeFromExtension() {
         assertEquals("audio/mpeg", mMimeTypeMap.getMimeTypeFromExtension("mp3"));
         assertEquals("application/zip", mMimeTypeMap.getMimeTypeFromExtension("zip"));
@@ -62,8 +81,9 @@
 
         assertNull(mMimeTypeMap.getMimeTypeFromExtension(null));
         assertNull(mMimeTypeMap.getMimeTypeFromExtension(""));
-}
+    }
 
+    @Test
     public void testHasExtension() {
         assertTrue(mMimeTypeMap.hasExtension("mp3"));
         assertTrue(mMimeTypeMap.hasExtension("zip"));
@@ -74,6 +94,7 @@
         assertFalse(mMimeTypeMap.hasExtension(null));
     }
 
+    @Test
     public void testGetExtensionFromMimeType() {
         assertEquals("mp3", mMimeTypeMap.getExtensionFromMimeType("audio/mpeg"));
         assertEquals("png", mMimeTypeMap.getExtensionFromMimeType("image/png"));
@@ -83,8 +104,9 @@
 
         assertNull(mMimeTypeMap.getExtensionFromMimeType(null));
         assertNull(mMimeTypeMap.getExtensionFromMimeType(""));
-}
+    }
 
+    @Test
     public void testGetSingleton() {
         MimeTypeMap firstMimeTypeMap = MimeTypeMap.getSingleton();
         MimeTypeMap secondMimeTypeMap = MimeTypeMap.getSingleton();
diff --git a/tests/tests/webkit/src/android/webkit/cts/PacProcessorTest.java b/tests/tests/webkit/src/android/webkit/cts/PacProcessorTest.java
index 26f699d..b8b7c0bb 100644
--- a/tests/tests/webkit/src/android/webkit/cts/PacProcessorTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/PacProcessorTest.java
@@ -20,16 +20,22 @@
 import android.net.ConnectivityManager;
 import android.net.Network;
 import android.webkit.PacProcessor;
-import com.android.compatibility.common.util.NullWebViewUtils;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.NullWebViewUtils;
+
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
+@MediumTest
+@RunWith(AndroidJUnit4.class)
 public final class PacProcessorTest {
 
     private TestProcessClient mProcess;
diff --git a/tests/tests/webkit/src/android/webkit/cts/PostMessageTest.java b/tests/tests/webkit/src/android/webkit/cts/PostMessageTest.java
index da5664e..ba1b354 100644
--- a/tests/tests/webkit/src/android/webkit/cts/PostMessageTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/PostMessageTest.java
@@ -16,52 +16,86 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
-import android.net.Uri;
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.WebMessage;
 import android.webkit.WebMessagePort;
 import android.webkit.WebView;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
+
 import com.google.common.util.concurrent.SettableFuture;
 
 import junit.framework.Assert;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 
-public class PostMessageTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class PostMessageTest extends SharedWebViewTest {
     private WebView mWebView;
     private WebViewOnUiThread mOnUiThread;
 
     private static final String WEBVIEW_MESSAGE = "from_webview";
     private static final String BASE_URI = "http://www.example.com";
 
-    public PostMessageTest() {
-        super("android.webkit.cts", WebViewCtsActivity.class);
-    }
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        final WebViewCtsActivity activity = getActivity();
-        mWebView = activity.getWebView();
+    @Before
+    public void setUp() throws Exception {
+        mWebView = getTestEnvironment().getWebView();
         if (mWebView != null) {
             mOnUiThread = new WebViewOnUiThread(mWebView);
             mOnUiThread.getSettings().setJavaScriptEnabled(true);
         }
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-        super.tearDown();
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                activity))
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
     }
 
     private static final String TITLE_FROM_POST_MESSAGE =
@@ -108,6 +142,7 @@
      * should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
     // Post a string message to main frame and make sure it is received.
+    @Test
     public void testSimpleMessageToMainFrame() throws Throwable {
         verifyPostMessageToOrigin(Uri.parse(BASE_URI));
     }
@@ -118,6 +153,7 @@
      * test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
     // Post a string message to main frame passing a wildcard as target origin
+    @Test
     public void testWildcardOriginMatchesAnything() throws Throwable {
         verifyPostMessageToOrigin(Uri.parse("*"));
     }
@@ -128,14 +164,12 @@
      * this test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
     // Post a string message to main frame passing an empty string as target origin
+    @Test
     public void testEmptyStringOriginMatchesAnything() throws Throwable {
         verifyPostMessageToOrigin(Uri.parse(""));
     }
 
     private void verifyPostMessageToOrigin(Uri origin) throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         loadPage(TITLE_FROM_POST_MESSAGE);
         WebMessage message = new WebMessage(WEBVIEW_MESSAGE);
         mOnUiThread.postWebMessage(message, origin);
@@ -149,10 +183,8 @@
      */
     // Post multiple messages to main frame and make sure they are received in
     // correct order.
+    @Test
     public void testMultipleMessagesToMainFrame() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         loadPage(TITLE_FROM_POST_MESSAGE);
         for (int i = 0; i < 10; i++) {
             mOnUiThread.postWebMessage(new WebMessage(Integer.toString(i)),
@@ -167,10 +199,8 @@
      * reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
     // Create a message channel and make sure it can be used for data transfer to/from js.
+    @Test
     public void testMessageChannel() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         loadPage(CHANNEL_MESSAGE);
         final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
         WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
@@ -205,10 +235,8 @@
      * that test as necessary. See http://go/modifying-webview-cts.
      */
     // Test that a message port that is closed cannot used to send a message
+    @Test
     public void testClose() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         loadPage(CHANNEL_MESSAGE);
         final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
         WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
@@ -249,11 +277,10 @@
      * be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
     // Test a message port created in JS can be received and used for message transfer.
+    @Test
     public void testReceiveMessagePort() throws Throwable {
         final String hello = "HELLO";
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+
         loadPage(CHANNEL_FROM_JS);
         final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
         WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
@@ -275,10 +302,8 @@
      * be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
     // Ensure the callback is invoked on the correct Handler.
+    @Test
     public void testWebMessageHandler() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         loadPage(CHANNEL_MESSAGE);
         final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
         WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
@@ -311,10 +336,8 @@
      * should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
     // Ensure the callback is invoked on the MainLooper by default.
+    @Test
     public void testWebMessageDefaultHandler() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         loadPage(CHANNEL_MESSAGE);
         final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
         WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
diff --git a/tests/tests/webkit/src/android/webkit/cts/ServiceWorkerClientTest.java b/tests/tests/webkit/src/android/webkit/cts/ServiceWorkerClientTest.java
index f1eb73d..3115dcf 100644
--- a/tests/tests/webkit/src/android/webkit/cts/ServiceWorkerClientTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/ServiceWorkerClientTest.java
@@ -16,28 +16,38 @@
 
 package android.webkit.cts;
 
-import android.test.ActivityInstrumentationTestCase2;
+import static org.junit.Assert.assertEquals;
 
 import android.webkit.JavascriptInterface;
-import android.webkit.ServiceWorkerController;
 import android.webkit.ServiceWorkerClient;
-import android.webkit.WebResourceResponse;
+import android.webkit.ServiceWorkerController;
 import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
 import android.webkit.WebView;
-import android.webkit.WebViewClient;
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.ByteArrayInputStream;
-import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Callable;
 
-
-public class ServiceWorkerClientTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ServiceWorkerClientTest extends SharedWebViewTest {
 
     // The BASE_URL does not matter since the tests will intercept the load, but it should be https
     // for the Service Worker registration to succeed.
@@ -74,13 +84,13 @@
             + "   console.error(err);"
             + "});";
 
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
     private JavascriptStatusReceiver mJavascriptStatusReceiver;
     private WebViewOnUiThread mOnUiThread;
 
-    public ServiceWorkerClientTest() throws Exception {
-        super("android.webkit.cts", WebViewCtsActivity.class);
-    }
-
     // Both this test and WebViewOnUiThread need to override some of the methods on WebViewClient,
     // so this test subclasses the WebViewClient from WebViewOnUiThread.
     private static class InterceptClient extends WaitForLoadedClient {
@@ -124,10 +134,9 @@
         }
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        WebView webview = getActivity().getWebView();
+    @Before
+    public void setUp() throws Exception {
+        WebView webview = getTestEnvironment().getWebView();
         if (webview == null) return;
         mOnUiThread = new WebViewOnUiThread(webview);
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
@@ -137,13 +146,33 @@
         mOnUiThread.setWebViewClient(new InterceptClient(mOnUiThread));
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
             ServiceWorkerController.getInstance().setServiceWorkerClient(null);
         }
-        super.tearDown();
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                activity))
+                                    .setContext(activity)
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
     }
 
     /**
@@ -153,11 +182,8 @@
      * http://go/modifying-webview-cts.
      */
     // Test correct invocation of shouldInterceptRequest for Service Workers.
+    @Test
     public void testServiceWorkerClientInterceptCallback() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final InterceptServiceWorkerClient mInterceptServiceWorkerClient =
                 new InterceptServiceWorkerClient();
         ServiceWorkerController swController = ServiceWorkerController.getInstance();
@@ -207,11 +233,8 @@
      * http://go/modifying-webview-cts.
      */
     // Test setting a null ServiceWorkerClient.
+    @Test
     public void testSetNullServiceWorkerClient() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         ServiceWorkerController swController = ServiceWorkerController.getInstance();
         swController.setServiceWorkerClient(null);
         mOnUiThread.loadUrlAndWaitForCompletion(INDEX_URL);
diff --git a/tests/tests/webkit/src/android/webkit/cts/ServiceWorkerWebSettingsTest.java b/tests/tests/webkit/src/android/webkit/cts/ServiceWorkerWebSettingsTest.java
index 8c28890..33ee187 100644
--- a/tests/tests/webkit/src/android/webkit/cts/ServiceWorkerWebSettingsTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/ServiceWorkerWebSettingsTest.java
@@ -16,43 +16,71 @@
 
 package android.webkit.cts;
 
-import android.content.pm.PackageManager;
-import android.os.Process;
-import android.test.ActivityInstrumentationTestCase2;
+import static org.junit.Assert.assertEquals;
+
 import android.webkit.ServiceWorkerController;
 import android.webkit.ServiceWorkerWebSettings;
 import android.webkit.WebSettings;
 import android.webkit.WebView;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
-public class ServiceWorkerWebSettingsTest extends
-        ActivityInstrumentationTestCase2<WebViewCtsActivity> {
-
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ServiceWorkerWebSettingsTest extends SharedWebViewTest {
     private ServiceWorkerWebSettings mSettings;
     private WebViewOnUiThread mOnUiThread;
 
-    public ServiceWorkerWebSettingsTest() {
-        super("android.webkit.cts", WebViewCtsActivity.class);
-    }
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        WebView webview = getActivity().getWebView();
+    @Before
+    public void setUp() throws Exception {
+        WebView webview = getTestEnvironment().getWebView();
         if (webview != null) {
             mOnUiThread = new WebViewOnUiThread(webview);
-            mSettings = ServiceWorkerController.getInstance().getServiceWorkerWebSettings();
         }
+        mSettings = ServiceWorkerController.getInstance().getServiceWorkerWebSettings();
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-        super.tearDown();
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                    activity))
+                                    .setContext(activity)
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
     }
 
     /**
@@ -60,11 +88,8 @@
      * androidx.webkit.ServiceWorkerWebSettingsCompatTest#testCacheMode. Modifications to this test
      * should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testCacheMode() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         int i = WebSettings.LOAD_DEFAULT;
         assertEquals(i, mSettings.getCacheMode());
         for (; i <= WebSettings.LOAD_CACHE_ONLY; i++) {
@@ -78,11 +103,8 @@
      * androidx.webkit.ServiceWorkerWebSettingsCompatTest#testAllowContentAccess. Modifications to
      * this test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testAllowContentAccess() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         assertEquals(mSettings.getAllowContentAccess(), true);
         for (boolean b : new boolean[]{false, true}) {
             mSettings.setAllowContentAccess(b);
@@ -95,11 +117,8 @@
      * androidx.webkit.ServiceWorkerWebSettingsCompatTest#testAllowFileAccess. Modifications to
      * this test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testAllowFileAccess() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         assertEquals(mSettings.getAllowFileAccess(), true);
         for (boolean b : new boolean[]{false, true}) {
             mSettings.setAllowFileAccess(b);
@@ -112,11 +131,8 @@
      * androidx.webkit.ServiceWorkerWebSettingsCompatTest#testBlockNetworkLoads. Modifications to
      * this test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testBlockNetworkLoads() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         // Note: we cannot test this setter unless we provide the INTERNET permission, otherwise we
         // get a SecurityException when we pass 'false'.
         final boolean hasInternetPermission = true;
diff --git a/tests/tests/webkit/src/android/webkit/cts/TestProcessClientTest.java b/tests/tests/webkit/src/android/webkit/cts/TestProcessClientTest.java
index f605f64..2183a15 100644
--- a/tests/tests/webkit/src/android/webkit/cts/TestProcessClientTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/TestProcessClientTest.java
@@ -18,12 +18,14 @@
 
 import android.content.Context;
 import android.os.Looper;
-import android.test.InstrumentationTestCase;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import junit.framework.Assert;
-import junit.framework.AssertionFailedError;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.io.IOException;
 
@@ -31,7 +33,9 @@
  * Test various scenarios of using {@link TestProcessService} and {@link TestProcessClient}
  * framework to run tests cases in freshly created test processes.
  */
-public class TestProcessClientTest extends InstrumentationTestCase {
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TestProcessClientTest {
 
     static class TestRunningOnUiThread extends TestProcessClient.UiThreadTestRunnable {
         @Override
@@ -60,6 +64,7 @@
         }
     }
 
+    @Test
     public void testRunDifferentRunnables() throws Throwable {
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         try (TestProcessClient process = TestProcessClient.createProcessA(context)) {
@@ -80,11 +85,12 @@
      * Test throwing an exception that is handled by the Parcel class: {@link
      * Parcel#writeException(java.lang.Exception)}.
      */
+    @Test
     public void testThrowingNullPointerException() throws Throwable {
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         try (TestProcessClient process = TestProcessClient.createProcessA(context)) {
             process.run(TestNullPointerException.class);
-            fail("A NullPointerException is expected to be thrown");
+            Assert.fail("A NullPointerException is expected to be thrown");
         } catch (NullPointerException e) {
 
         }
@@ -101,11 +107,12 @@
      * Test throwing an exception that is not handled by the Parcel class: {@link
      * Parcel#writeException(java.lang.Exception)}.
      */
+    @Test
     public void testThrowingIOException() throws Throwable {
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         try (TestProcessClient process = TestProcessClient.createProcessA(context)) {
             process.run(TestIOException.class);
-            fail("An IOException is expected to be thrown");
+            Assert.fail("An IOException is expected to be thrown");
         } catch (IOException e) {
         }
     }
@@ -113,19 +120,20 @@
     static class TestFailedAssertion extends TestProcessClient.TestRunnable {
         @Override
         public void run(Context ctx) throws Throwable {
-            fail("This assertion should be caught");
+            Assert.fail("This assertion should be caught");
         }
     }
 
     /**
      * Test that junit assertions failures are propagated as expected.
      */
+    @Test
     public void testFailedAssertion() throws Throwable {
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         try (TestProcessClient process = TestProcessClient.createProcessA(context)) {
             process.run(TestFailedAssertion.class);
-            fail("An AssertionFailedError is expected to be thrown");
-        } catch (AssertionFailedError e) {
+            Assert.fail("An AssertionError is expected to be thrown");
+        } catch (AssertionError e) {
             Assert.assertEquals("This assertion should be caught", e.getMessage());
         }
     }
diff --git a/tests/tests/webkit/src/android/webkit/cts/TracingControllerTest.java b/tests/tests/webkit/src/android/webkit/cts/TracingControllerTest.java
index fba7629..c9b2304 100644
--- a/tests/tests/webkit/src/android/webkit/cts/TracingControllerTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/TracingControllerTest.java
@@ -18,28 +18,43 @@
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.greaterThan;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.TracingConfig;
 import android.webkit.TracingController;
 import android.webkit.WebView;
-import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
-public class TracingControllerTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class TracingControllerTest {
 
     public static class TracingReceiver extends OutputStream {
         private int mChunkCount;
@@ -106,43 +121,45 @@
     private static final int EXECUTOR_TIMEOUT = 10; // timeout of executor shutdown in seconds
     private static final String EXECUTOR_THREAD_PREFIX = "TracingExecutorThread";
     private WebViewOnUiThread mOnUiThread;
-    private ExecutorService singleThreadExecutor;
+    private ExecutorService mSingleThreadExecutor;
 
-    public TracingControllerTest() throws Exception {
-        super("android.webkit.cts", WebViewCtsActivity.class);
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
+    @Before
+    public void setUp() throws Exception {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            WebViewCtsActivity webViewCtsActivity = (WebViewCtsActivity) activity;
+            WebView webview = webViewCtsActivity.getWebView();
+            if (webview != null) {
+                mOnUiThread = new WebViewOnUiThread(webview);
+            }
+        });
+        mSingleThreadExecutor = Executors.newSingleThreadExecutor(getCustomThreadFactory());
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        WebView webview = getActivity().getWebView();
-        if (webview == null) return;
-        mOnUiThread = new WebViewOnUiThread(webview);
-        singleThreadExecutor = Executors.newSingleThreadExecutor(getCustomThreadFactory());
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         // make sure to stop everything and clean up
-        ensureTracingStopped();
-        if (singleThreadExecutor != null) {
-            singleThreadExecutor.shutdown();
-            if (!singleThreadExecutor.awaitTermination(EXECUTOR_TIMEOUT, TimeUnit.SECONDS)) {
+        if (NullWebViewUtils.isWebViewAvailable()) {
+            ensureTracingStopped();
+        }
+
+        if (mSingleThreadExecutor != null) {
+            mSingleThreadExecutor.shutdown();
+            if (!mSingleThreadExecutor.awaitTermination(EXECUTOR_TIMEOUT, TimeUnit.SECONDS)) {
                 fail("Failed to shutdown executor");
             }
         }
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-        super.tearDown();
     }
 
     private void ensureTracingStopped() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        TracingController.getInstance().stop(null, singleThreadExecutor);
+        TracingController.getInstance().stop(null, mSingleThreadExecutor);
         Callable<Boolean> tracingStopped = new Callable<Boolean>() {
             @Override
             public Boolean call() {
@@ -166,14 +183,12 @@
 
     // Test that callbacks are invoked and tracing data is returned on the correct thread
     // (via executor). Tracing start/stop and webview loading happens on the UI thread.
+    @Test
     public void testTracingControllerCallbacksOnUI() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final TracingReceiver tracingReceiver = new TracingReceiver();
 
         WebkitUtils.onMainThreadSync(() -> {
-            runTracingTestWithCallbacks(tracingReceiver, singleThreadExecutor);
+            runTracingTestWithCallbacks(tracingReceiver, mSingleThreadExecutor);
         });
         PollingCheck.check("Tracing did not complete", POLLING_TIMEOUT, tracingReceiver.getCompleteCallable());
         assertThat(tracingReceiver.getNbChunks(), greaterThan(0));
@@ -185,35 +200,26 @@
     // Test that callbacks are invoked and tracing data is returned on the correct thread
     // (via executor). Tracing start/stop happens on the testing thread; webview loading
     // happens on the UI thread.
+    @Test
     public void testTracingControllerCallbacks() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final TracingReceiver tracingReceiver = new TracingReceiver();
-        runTracingTestWithCallbacks(tracingReceiver, singleThreadExecutor);
+        runTracingTestWithCallbacks(tracingReceiver, mSingleThreadExecutor);
         PollingCheck.check("Tracing did not complete", POLLING_TIMEOUT, tracingReceiver.getCompleteCallable());
         assertThat(tracingReceiver.getNbChunks(), greaterThan(0));
         assertThat(tracingReceiver.getOutputStream().size(), greaterThan(0));
     }
 
     // Test that tracing stop has no effect if tracing has not been started.
+    @Test
     public void testTracingStopFalseIfNotTracing() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         TracingController tracingController = TracingController.getInstance();
-        assertFalse(tracingController.stop(null, singleThreadExecutor));
+        assertFalse(tracingController.stop(null, mSingleThreadExecutor));
         assertFalse(tracingController.isTracing());
     }
 
     // Test that tracing cannot be started if already tracing.
+    @Test
     public void testTracingCannotStartIfAlreadyTracing() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         TracingController tracingController = TracingController.getInstance();
         TracingConfig config = new TracingConfig.Builder().build();
 
@@ -225,16 +231,13 @@
             // as expected
             return;
         }
-        assertTrue(tracingController.stop(null, singleThreadExecutor));
+        assertTrue(tracingController.stop(null, mSingleThreadExecutor));
         fail("Tracing start should throw an exception when attempting to start while already tracing");
     }
 
     // Test that tracing cannot be invoked with excluded categories.
+    @Test
     public void testTracingInvalidCategoriesPatternExclusion() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         TracingController tracingController = TracingController.getInstance();
         TracingConfig config = new TracingConfig.Builder()
                 .addCategories("android_webview","-blink")
@@ -251,11 +254,8 @@
     }
 
     // Test that tracing cannot be invoked with categories containing commas.
+    @Test
     public void testTracingInvalidCategoriesPatternComma() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         TracingController tracingController = TracingController.getInstance();
         TracingConfig config = new TracingConfig.Builder()
                 .addCategories("android_webview, blink")
@@ -272,11 +272,8 @@
     }
 
     // Test that tracing cannot start with a configuration that is null.
+    @Test
     public void testTracingWithNullConfig() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         TracingController tracingController = TracingController.getInstance();
         try {
             tracingController.start(null);
diff --git a/tests/tests/webkit/src/android/webkit/cts/URLUtilTest.java b/tests/tests/webkit/src/android/webkit/cts/URLUtilTest.java
index 34ae10a..061b768 100644
--- a/tests/tests/webkit/src/android/webkit/cts/URLUtilTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/URLUtilTest.java
@@ -16,12 +16,25 @@
 
 package android.webkit.cts;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
-import android.test.AndroidTestCase;
-import android.test.MoreAsserts;
 import android.webkit.URLUtil;
 
-public class URLUtilTest extends AndroidTestCase {
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class URLUtilTest extends SharedWebViewTest {
     private final String VALID_HTTP_URL = "http://www.google.com";
     private final String VALID_HTTPS_URL = "https://www.google.com";
     private final String VALID_ASSET_URL = "file:///android_asset/test";
@@ -34,18 +47,26 @@
     private final String VALID_FTP_URL = "ftp://www.domain.com";
     private final String FILE_URL_NO_SLASH = "file:test";
 
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        return new SharedWebViewTestEnvironment.Builder().build();
+    }
+
+    @Test
     public void testIsAssetUrl() {
         assertFalse(URLUtil.isAssetUrl(null));
         assertFalse(URLUtil.isAssetUrl(VALID_HTTP_URL));
         assertTrue(URLUtil.isAssetUrl(VALID_ASSET_URL));
     }
 
+    @Test
     public void testIsAboutUrl() {
         assertFalse(URLUtil.isAboutUrl(null));
         assertFalse(URLUtil.isAboutUrl(VALID_DATA_URL));
         assertTrue(URLUtil.isAboutUrl(VALID_ABOUT_URL));
     }
 
+    @Test
     public void testIsContentUrl() {
         assertFalse(URLUtil.isContentUrl(null));
         assertFalse(URLUtil.isContentUrl(VALID_DATA_URL));
@@ -53,18 +74,21 @@
     }
 
     @SuppressWarnings("deprecation")
+    @Test
     public void testIsCookielessProxyUrl() {
         assertFalse(URLUtil.isCookielessProxyUrl(null));
         assertFalse(URLUtil.isCookielessProxyUrl(VALID_HTTP_URL));
         assertTrue(URLUtil.isCookielessProxyUrl(VALID_PROXY_URL));
     }
 
+    @Test
     public void testIsDataUrl() {
         assertFalse(URLUtil.isDataUrl(null));
         assertFalse(URLUtil.isDataUrl(VALID_CONTENT_URL));
         assertTrue(URLUtil.isDataUrl(VALID_DATA_URL));
     }
 
+    @Test
     public void testIsFileUrl() {
         assertFalse(URLUtil.isFileUrl(null));
         assertFalse(URLUtil.isFileUrl(VALID_CONTENT_URL));
@@ -76,24 +100,28 @@
         assertTrue(URLUtil.isFileUrl(FILE_URL_NO_SLASH));
     }
 
+    @Test
     public void testIsHttpsUrl() {
         assertFalse(URLUtil.isHttpsUrl(null));
         assertFalse(URLUtil.isHttpsUrl(VALID_HTTP_URL));
         assertTrue(URLUtil.isHttpsUrl(VALID_HTTPS_URL));
     }
 
+    @Test
     public void testIsHttpUrl() {
         assertFalse(URLUtil.isHttpUrl(null));
         assertFalse(URLUtil.isHttpUrl(VALID_FTP_URL));
         assertTrue(URLUtil.isHttpUrl(VALID_HTTP_URL));
     }
 
+    @Test
     public void testIsJavaScriptUrl() {
         assertFalse(URLUtil.isJavaScriptUrl(null));
         assertFalse(URLUtil.isJavaScriptUrl(VALID_FTP_URL));
         assertTrue(URLUtil.isJavaScriptUrl(VALID_JAVASCRIPT_URL));
     }
 
+    @Test
     public void testIsNetworkUrl() {
         assertFalse(URLUtil.isNetworkUrl(null));
         assertFalse(URLUtil.isNetworkUrl(""));
@@ -102,6 +130,7 @@
         assertTrue(URLUtil.isNetworkUrl(VALID_HTTPS_URL));
     }
 
+    @Test
     public void testIsValidUrl() {
         assertFalse(URLUtil.isValidUrl(null));
         assertFalse(URLUtil.isValidUrl(""));
@@ -115,6 +144,7 @@
         assertTrue(URLUtil.isValidUrl(VALID_CONTENT_URL));
     }
 
+    @Test
     public void testComposeSearchUrl() {
         assertNull(URLUtil.composeSearchUrl("", "template", "no such holder"));
 
@@ -125,6 +155,7 @@
         assertEquals(expected, URLUtil.composeSearchUrl("query", "file://holder/test", "holder"));
     }
 
+    @Test
     public void testDecode() {
         byte[] url = new byte[0];
         byte[] result = URLUtil.decode(url);
@@ -135,17 +166,17 @@
         result = URLUtil.decode(url);
         byte[] expected = new byte[] { 'w', 'w', 'w', '.', 'n', 'a', 'm', 'e', '.', 'c', 'o', 'm',
                 '/', ' ', 'E', '/' };
-        MoreAsserts.assertEquals(expected, result);
+        assertThat(result, is(expected));
 
-        url = new byte[] { 'w', 'w', 'w', '.', 'n', 'a', 'm', 'e', '.', 'c', 'o', 'm', '/',
-                '%', '2', '0', '%', '4' };
-        try {
-            result = URLUtil.decode(url);
-            fail("should throw IllegalArgumentException.");
-        } catch (IllegalArgumentException e) {
-        }
+        // '%4' is an incomplete sequence and should trigger an Invalid format error.
+        final byte[] urlWithIncompletePercentSequence = new byte[] { 'w', 'w', 'w', '.',
+            'n', 'a', 'm', 'e', '.', 'c', 'o', 'm', '/', '%', '2', '0', '%', '4' };
+        assertThrows(IllegalArgumentException.class, () -> {
+            URLUtil.decode(urlWithIncompletePercentSequence);
+        });
     }
 
+    @Test
     public void testGuessFileName() {
         String url = "ftp://example.url/test";
         assertEquals("test.jpg", URLUtil.guessFileName(url, null, "image/jpeg"));
@@ -153,6 +184,7 @@
         assertEquals("test.bin", URLUtil.guessFileName(url, null, "application/octet-stream"));
     }
 
+    @Test
     public void testGuessUrl() {
         assertEquals(VALID_FILE_URL, URLUtil.guessUrl(VALID_FILE_URL));
         assertEquals(VALID_ABOUT_URL, URLUtil.guessUrl(VALID_ABOUT_URL));
@@ -162,13 +194,12 @@
         String url = "domainName";
         assertEquals("http://www.domainName.com/", URLUtil.guessUrl(url));
 
-        try {
+        assertThrows(NullPointerException.class, () -> {
             URLUtil.guessUrl(null);
-            fail("should throw NullPointerException.");
-        } catch (NullPointerException e) {
-        }
+        });
     }
 
+    @Test
     public void testStripAnchor() {
         assertEquals(VALID_HTTP_URL, URLUtil.stripAnchor(VALID_HTTP_URL));
 
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebBackForwardListTest.java b/tests/tests/webkit/src/android/webkit/cts/WebBackForwardListTest.java
index b40e672..e20a6b1 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebBackForwardListTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebBackForwardListTest.java
@@ -16,45 +16,77 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.WebBackForwardList;
 import android.webkit.WebHistoryItem;
 import android.webkit.WebView;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
-public class WebBackForwardListTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebBackForwardListTest extends SharedWebViewTest {
+
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
 
     private WebViewOnUiThread mOnUiThread;
 
-    public WebBackForwardListTest() {
-        super("android.webkit.cts", WebViewCtsActivity.class);
-    }
-
-    @Override
+    @Before
     public void setUp() throws Exception {
-        super.setUp();
-        WebView webview = getActivity().getWebView();
+        WebView webview = getTestEnvironment().getWebView();
         if (webview != null) {
             mOnUiThread = new WebViewOnUiThread(webview);
         }
     }
 
-    @Override
+    @After
     public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-        super.tearDown();
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                activity))
+                                    .setContext(activity)
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
     }
 
     @AppModeFull(reason = "Instant apps cannot bind sockets")
+    @Test
     public void testGetCurrentItem() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         WebBackForwardList list = mOnUiThread.copyBackForwardList();
 
         assertNull(list.getCurrentItem());
@@ -63,7 +95,7 @@
         assertNull(list.getItemAtIndex(-1));
         assertNull(list.getItemAtIndex(2));
 
-        CtsTestServer server = new CtsTestServer(getActivity(), false);
+        SharedSdkWebServer server = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         try {
             String url1 = server.getAssetUrl(TestHtmlConstants.HTML_URL1);
             String url2 = server.getAssetUrl(TestHtmlConstants.HTML_URL2);
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebChromeClientTest.java b/tests/tests/webkit/src/android/webkit/cts/WebChromeClientTest.java
index f7528c3..c753bbd 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebChromeClientTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebChromeClientTest.java
@@ -16,17 +16,21 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
 import android.graphics.Bitmap;
 import android.os.Message;
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.util.Base64;
 import android.view.MotionEvent;
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.webkit.ConsoleMessage;
-import android.view.ViewParent;
 import android.webkit.JsPromptResult;
 import android.webkit.JsResult;
 import android.webkit.WebIconDatabase;
@@ -34,44 +38,54 @@
 import android.webkit.WebView;
 import android.webkit.cts.WebViewSyncLoader.WaitForProgressClient;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
+
 import com.google.common.util.concurrent.SettableFuture;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
 
 @AppModeFull
-public class WebChromeClientTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebChromeClientTest extends SharedWebViewTest{
     private static final String JAVASCRIPT_UNLOAD = "javascript unload";
     private static final String LISTENER_ADDED = "listener added";
     private static final String TOUCH_RECEIVED = "touch received";
 
-    private CtsTestServer mWebServer;
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
+    private SharedSdkWebServer mWebServer;
     private WebIconDatabase mIconDb;
     private WebViewOnUiThread mOnUiThread;
     private boolean mBlockWindowCreationSync;
     private boolean mBlockWindowCreationAsync;
 
-    public WebChromeClientTest() {
-        super(WebViewCtsActivity.class);
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        WebView webview = getActivity().getWebView();
+    @Before
+    public void setUp() throws Exception {
+        WebView webview = getTestEnvironment().getWebView();
         if (webview != null) {
             mOnUiThread = new WebViewOnUiThread(webview);
         }
-        mWebServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
@@ -82,13 +96,34 @@
             mIconDb.removeAllIcons();
             mIconDb.close();
         }
-        super.tearDown();
     }
 
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                    activity))
+                                    .setContext(activity)
+                                    .setWebView(webView)
+                                    .setRootLayout(((WebViewCtsActivity) activity).getRootLayout());
+                        });
+
+        SharedWebViewTestEnvironment environment = builder.build();
+        return environment;
+    }
+
+
+    @Test
     public void testOnProgressChanged() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
         mOnUiThread.setWebChromeClient(webChromeClient);
 
@@ -104,10 +139,8 @@
         }.run();
     }
 
+    @Test
     public void testOnReceivedTitle() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
         mOnUiThread.setWebChromeClient(webChromeClient);
 
@@ -125,20 +158,18 @@
         assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, webChromeClient.getPageTitle());
     }
 
+    @Test
     public void testOnReceivedIcon() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
         mOnUiThread.setWebChromeClient(webChromeClient);
 
         WebkitUtils.onMainThreadSync(() -> {
             // getInstance must run on the UI thread
             mIconDb = WebIconDatabase.getInstance();
-            String dbPath = getActivity().getFilesDir().toString() + "/icons";
+            String dbPath = getTestEnvironment().getContext().getFilesDir().toString() + "/icons";
             mIconDb.open(dbPath);
         });
-        getInstrumentation().waitForIdleSync();
+        getTestEnvironment().waitForIdleSync();
         Thread.sleep(100); // Wait for open to be received on the icon db thread.
 
         assertFalse(webChromeClient.hadOnReceivedIcon());
@@ -190,35 +221,26 @@
             assertFalse(webChromeClient.hadOnCloseWindow());
         }
     }
+    @Test
     public void testWindows() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         runWindowTest(true);
     }
 
+    @Test
     public void testBlockWindowsSync() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mBlockWindowCreationSync = true;
         runWindowTest(false);
     }
 
+    @Test
     public void testBlockWindowsAsync() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mBlockWindowCreationAsync = true;
         runWindowTest(false);
     }
 
     // Note that test is still a little flaky. See b/119468441.
+    @Test
     public void testOnJsBeforeUnloadIsCalled() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final WebSettings settings = mOnUiThread.getSettings();
         settings.setJavaScriptEnabled(true);
         settings.setJavaScriptCanOpenWindowsAutomatically(true);
@@ -258,10 +280,8 @@
         WebkitUtils.waitForFuture(onJsBeforeUnloadFuture);
     }
 
+    @Test
     public void testOnJsAlert() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
         mOnUiThread.setWebChromeClient(webChromeClient);
 
@@ -283,10 +303,8 @@
         assertEquals(webChromeClient.getMessage(), "testOnJsAlert");
     }
 
+    @Test
     public void testOnJsConfirm() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
         mOnUiThread.setWebChromeClient(webChromeClient);
 
@@ -308,10 +326,8 @@
         assertEquals(webChromeClient.getMessage(), "testOnJsConfirm");
     }
 
+    @Test
     public void testOnJsPrompt() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
         mOnUiThread.setWebChromeClient(webChromeClient);
 
@@ -342,10 +358,8 @@
         assertEquals(webChromeClient.getMessage(), "testOnJsPrompt");
     }
 
+    @Test
     public void testOnConsoleMessage() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         int numConsoleMessages = 4;
         final BlockingQueue<ConsoleMessage> consoleMessageQueue =
                 new ArrayBlockingQueue<>(numConsoleMessages);
@@ -411,17 +425,17 @@
         int middleY = location[1] + mOnUiThread.getWebView().getHeight() / 2;
 
         long timeDown = SystemClock.uptimeMillis();
-        getInstrumentation().sendPointerSync(
+        getTestEnvironment().sendPointerSync(
                 MotionEvent.obtain(timeDown, timeDown, MotionEvent.ACTION_DOWN,
                         middleX, middleY, 0));
 
         long timeUp = SystemClock.uptimeMillis();
-        getInstrumentation().sendPointerSync(
+        getTestEnvironment().sendPointerSync(
                 MotionEvent.obtain(timeUp, timeUp, MotionEvent.ACTION_UP,
                         middleX, middleY, 0));
 
         // Wait for the system to process all events in the queue
-        getInstrumentation().waitForIdleSync();
+        getTestEnvironment().waitForIdleSync();
     }
 
     private class MockWebChromeClient extends WaitForProgressClient {
@@ -572,12 +586,12 @@
             if (mBlockWindowCreationAsync) {
                 transport.setWebView(null);
             } else {
-                mChildWebView = new WebView(getActivity());
+                mChildWebView = new WebView(getTestEnvironment().getContext());
                 final WebSettings settings = mChildWebView.getSettings();
                 settings.setJavaScriptEnabled(true);
                 mChildWebView.setWebChromeClient(this);
                 transport.setWebView(mChildWebView);
-                getActivity().addContentView(mChildWebView, new ViewGroup.LayoutParams(
+                getTestEnvironment().addContentView(mChildWebView, new ViewGroup.LayoutParams(
                         ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
             }
             resultMsg.sendToTarget();
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebHistoryItemTest.java b/tests/tests/webkit/src/android/webkit/cts/WebHistoryItemTest.java
index 45529ec..d0881d9 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebHistoryItemTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebHistoryItemTest.java
@@ -16,23 +16,41 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
 import android.graphics.Bitmap;
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.WebBackForwardList;
-import android.webkit.cts.WebViewSyncLoader.WaitForProgressClient;
 import android.webkit.WebHistoryItem;
 import android.webkit.WebIconDatabase;
 import android.webkit.WebView;
+import android.webkit.cts.WebViewSyncLoader.WaitForProgressClient;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 @AppModeFull
-public class WebHistoryItemTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
-    private CtsTestServer mWebServer;
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebHistoryItemTest extends SharedWebViewTest {
+    private SharedSdkWebServer mWebServer;
     private WebViewOnUiThread mOnUiThread;
     private WebIconDatabase mIconDb;
+    private Context mContext;
 
     class WaitForIconClient extends WaitForProgressClient {
         private boolean mReceivedIcon;
@@ -49,43 +67,63 @@
         public synchronized boolean receivedIcon() { return mReceivedIcon; }
     };
 
-    public WebHistoryItemTest() {
-        super("android.webkit.cts", WebViewCtsActivity.class);
-    }
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mWebServer = new CtsTestServer(getActivity());
-        WebView webview = getActivity().getWebView();
+    @Before
+    public void setUp() throws Exception {
+        WebView webview = getTestEnvironment().getWebView();
         if (webview != null) {
             mOnUiThread = new WebViewOnUiThread(webview);
         }
+        mContext = getTestEnvironment().getContext();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-        mWebServer.shutdown();
-        super.tearDown();
+        if (mWebServer != null) {
+            mWebServer.shutdown();
+        }
         if (mIconDb != null) {
             mIconDb.removeAllIcons();
             mIconDb.close();
         }
     }
 
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                activity))
+                                    .setContext(activity)
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
+    }
+
+    @Test
     public void testWebHistoryItem() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final WaitForIconClient waitForIconClient = new WaitForIconClient(mOnUiThread);
         mOnUiThread.setWebChromeClient(waitForIconClient);
         WebkitUtils.onMainThreadSync(() -> {
             // getInstance must run on the UI thread
             mIconDb = WebIconDatabase.getInstance();
-            String dbPath = getActivity().getFilesDir().toString() + "/icons";
+            String dbPath = mContext.getFilesDir().toString() + "/icons";
             mIconDb.open(dbPath);
         });
 
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebSettingsTest.java b/tests/tests/webkit/src/android/webkit/cts/WebSettingsTest.java
index e7fd40e..d44cbb0 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebSettingsTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebSettingsTest.java
@@ -18,7 +18,11 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import android.content.Context;
 import android.graphics.Bitmap;
@@ -26,7 +30,6 @@
 import android.os.Build;
 import android.os.Message;
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.util.Base64;
 import android.view.ViewGroup;
 import android.webkit.SslErrorHandler;
@@ -41,11 +44,22 @@
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
 import android.webkit.cts.WebViewSyncLoader.WaitForProgressClient;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
 import com.google.common.util.concurrent.SettableFuture;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.ByteArrayInputStream;
 import java.io.FileOutputStream;
 import java.nio.charset.StandardCharsets;
@@ -58,7 +72,9 @@
  * Tests for {@link android.webkit.WebSettings}
  */
 @AppModeFull
-public class WebSettingsTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebSettingsTest extends SharedWebViewTest {
     private static final String LOG_TAG = "WebSettingsTest";
 
     private final String EMPTY_IMAGE_HEIGHT = "0";
@@ -73,35 +89,56 @@
             "'></body></html>";
     private final String DATA_URL_IMAGE_HEIGHT = "1";
 
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
     private WebSettings mSettings;
-    private CtsTestServer mWebServer;
+    private SharedSdkWebServer mWebServer;
     private WebViewOnUiThread mOnUiThread;
     private Context mContext;
 
-    public WebSettingsTest() {
-        super("android.webkit.cts", WebViewCtsActivity.class);
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        WebView webview = getActivity().getWebView();
+    @Before
+    public void setUp() throws Exception {
+        WebView webview = getTestEnvironment().getWebView();
         if (webview != null) {
             mOnUiThread = new WebViewOnUiThread(webview);
-            mSettings = mOnUiThread.getSettings();
         }
-        mContext = getInstrumentation().getTargetContext();
+        mSettings = mOnUiThread.getSettings();
+        mContext = getTestEnvironment().getContext();
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+
+    @After
+    public void tearDown() throws Exception {
         if (mWebServer != null) {
             mWebServer.shutdown();
         }
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-        super.tearDown();
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                    activity))
+                                    .setContext(activity)
+                                    .setWebView(webView)
+                                    .setRootLayout(((WebViewCtsActivity) activity).getRootLayout());
+                        });
+
+        return builder.build();
     }
 
     /**
@@ -113,16 +150,15 @@
      * AppleWebKit/<major>.<minor> (KHTML, like Gecko) Version/<major>.<minor>
      * Chrome/<major>.<minor>.<branch>.<build>[ Mobile] Safari/<major>.<minor>
      */
+    @Test
     public void testUserAgentString_default() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         checkUserAgentStringHelper(mSettings.getUserAgentString(), true);
     }
 
     /**
      * Verifies that the useragent testing regex is actually correct, because it's very complex.
      */
+    @Test
     public void testUserAgentStringTest() {
         // All test UAs share the same prefix and suffix; only the middle part varies.
         final String prefix = "Mozilla/5.0 (Linux; Android " + Build.VERSION.RELEASE + "; ";
@@ -196,11 +232,9 @@
         }
     }
 
+    @Test
     public void testAccessUserAgentString() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         String url = mWebServer.getUserAgentUrl();
 
         String defaultUserAgent = mSettings.getUserAgentString();
@@ -227,11 +261,8 @@
         assertEquals(customUserAgent, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testAccessAllowFileAccess() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         // prepare an HTML file in the data directory we can access to test the setting.
         final String dataDirTitle = "Loaded from data dir";
         final String dataDirFile = "datadir.html";
@@ -264,10 +295,8 @@
                 dataDirTitle.equals(mOnUiThread.getTitle()));
     }
 
+    @Test
     public void testAccessCacheMode_defaultValue() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertEquals(WebSettings.LOAD_DEFAULT, mSettings.getCacheMode());
     }
 
@@ -275,21 +304,19 @@
         WebkitUtils.onMainThreadSync(() -> {
             // getInstance must run on the UI thread
             WebIconDatabase iconDb = WebIconDatabase.getInstance();
-            String dbPath = getActivity().getFilesDir().toString() + "/icons";
+            String dbPath = mContext.getFilesDir().toString() + "/icons";
             iconDb.open(dbPath);
         });
-        getInstrumentation().waitForIdleSync();
+        getTestEnvironment().waitForIdleSync();
         Thread.sleep(100); // Wait for open to be received on the icon db thread.
     }
 
+    @Test
     public void testAccessCacheMode_cacheElseNetwork() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         openIconDatabase();
         final IconListenerClient iconListener = new IconListenerClient();
         mOnUiThread.setWebChromeClient(iconListener);
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
         mSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
         assertEquals(WebSettings.LOAD_CACHE_ELSE_NETWORK, mSettings.getCacheMode());
@@ -307,14 +334,12 @@
                 requestCountAfterSecondLoad, requestCountAfterFirstLoad);
     }
 
+    @Test
     public void testAccessCacheMode_noCache() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         openIconDatabase();
         final IconListenerClient iconListener = new IconListenerClient();
         mOnUiThread.setWebChromeClient(iconListener);
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
         mSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
         assertEquals(WebSettings.LOAD_NO_CACHE, mSettings.getCacheMode());
@@ -332,14 +357,12 @@
                 requestCountAfterSecondLoad > requestCountAfterFirstLoad);
     }
 
+    @Test
     public void testAccessCacheMode_cacheOnly() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         openIconDatabase();
         final IconListenerClient iconListener = new IconListenerClient();
         mOnUiThread.setWebChromeClient(iconListener);
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
         // As a precondition, get the icon in the cache.
         mSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
@@ -357,10 +380,8 @@
                 requestCountAfterFirstLoad, initialRequestCount);
     }
 
+    @Test
     public void testAccessCursiveFontFamily() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertNotNull(mSettings.getCursiveFontFamily());
 
         String newCusiveFamily = "Apple Chancery";
@@ -368,10 +389,8 @@
         assertEquals(newCusiveFamily, mSettings.getCursiveFontFamily());
     }
 
+    @Test
     public void testAccessFantasyFontFamily() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertNotNull(mSettings.getFantasyFontFamily());
 
         String newFantasyFamily = "Papyrus";
@@ -379,10 +398,8 @@
         assertEquals(newFantasyFamily, mSettings.getFantasyFontFamily());
     }
 
+    @Test
     public void testAccessFixedFontFamily() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertNotNull(mSettings.getFixedFontFamily());
 
         String newFixedFamily = "Courier";
@@ -390,10 +407,8 @@
         assertEquals(newFixedFamily, mSettings.getFixedFontFamily());
     }
 
+    @Test
     public void testAccessSansSerifFontFamily() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertNotNull(mSettings.getSansSerifFontFamily());
 
         String newFixedFamily = "Verdana";
@@ -401,10 +416,8 @@
         assertEquals(newFixedFamily, mSettings.getSansSerifFontFamily());
     }
 
+    @Test
     public void testAccessSerifFontFamily() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertNotNull(mSettings.getSerifFontFamily());
 
         String newSerifFamily = "Times";
@@ -412,10 +425,8 @@
         assertEquals(newSerifFamily, mSettings.getSerifFontFamily());
     }
 
+    @Test
     public void testAccessStandardFontFamily() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertNotNull(mSettings.getStandardFontFamily());
 
         String newStandardFamily = "Times";
@@ -423,10 +434,8 @@
         assertEquals(newStandardFamily, mSettings.getStandardFontFamily());
     }
 
+    @Test
     public void testAccessDefaultFontSize() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         int defaultSize = mSettings.getDefaultFontSize();
         assertThat(defaultSize, greaterThan(0));
 
@@ -446,10 +455,8 @@
         assertEquals(10, mSettings.getDefaultFontSize());
     }
 
+    @Test
     public void testAccessDefaultFixedFontSize() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         int defaultSize = mSettings.getDefaultFixedFontSize();
         assertThat(defaultSize, greaterThan(0));
 
@@ -469,10 +476,8 @@
         assertEquals(10, mSettings.getDefaultFixedFontSize());
     }
 
+    @Test
     public void testAccessDefaultTextEncodingName() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertNotNull(mSettings.getDefaultTextEncodingName());
 
         String newEncodingName = "iso-8859-1";
@@ -480,13 +485,11 @@
         assertEquals(newEncodingName, mSettings.getDefaultTextEncodingName());
     }
 
+    @Test
     public void testAccessJavaScriptCanOpenWindowsAutomatically() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mSettings.setJavaScriptEnabled(true);
         mSettings.setSupportMultipleWindows(true);
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
         final WebView childWebView = mOnUiThread.createWebView();
         final SettableFuture<Void> createWindowFuture = SettableFuture.create();
@@ -519,10 +522,8 @@
         WebkitUtils.waitForFuture(createWindowFuture);
     }
 
+    @Test
     public void testAccessJavaScriptEnabled() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mSettings.setJavaScriptEnabled(true);
         assertTrue(mSettings.getJavaScriptEnabled());
         loadAssetUrl(TestHtmlConstants.JAVASCRIPT_URL);
@@ -545,10 +546,8 @@
 
     }
 
+    @Test
     public void testAccessLayoutAlgorithm() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertEquals(WebSettings.LayoutAlgorithm.NARROW_COLUMNS, mSettings.getLayoutAlgorithm());
 
         mSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
@@ -558,10 +557,8 @@
         assertEquals(WebSettings.LayoutAlgorithm.SINGLE_COLUMN, mSettings.getLayoutAlgorithm());
     }
 
+    @Test
     public void testAccessMinimumFontSize() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertEquals(8, mSettings.getMinimumFontSize());
 
         mSettings.setMinimumFontSize(100);
@@ -574,10 +571,8 @@
         assertEquals(10, mSettings.getMinimumFontSize());
     }
 
+    @Test
     public void testAccessMinimumLogicalFontSize() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertEquals(8, mSettings.getMinimumLogicalFontSize());
 
         mSettings.setMinimumLogicalFontSize(100);
@@ -590,10 +585,8 @@
         assertEquals(10, mSettings.getMinimumLogicalFontSize());
     }
 
+    @Test
     public void testAccessPluginsEnabled() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertFalse(mSettings.getPluginsEnabled());
 
         mSettings.setPluginsEnabled(true);
@@ -605,20 +598,16 @@
      * androidx.webkit.WebSettingsCompatTest#testOffscreenPreRaster. Modifications to this test
      * should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testOffscreenPreRaster() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertFalse(mSettings.getOffscreenPreRaster());
 
         mSettings.setOffscreenPreRaster(true);
         assertTrue(mSettings.getOffscreenPreRaster());
     }
 
+    @Test
     public void testAccessPluginsPath() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertNotNull(mSettings.getPluginsPath());
 
         String pluginPath = "pluginPath";
@@ -626,10 +615,8 @@
         assertEquals("Plugin path always empty", "", mSettings.getPluginsPath());
     }
 
+    @Test
     public void testAccessTextSize() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mSettings.setTextSize(TextSize.NORMAL);
         assertEquals(TextSize.NORMAL, mSettings.getTextSize());
 
@@ -646,39 +633,31 @@
         assertEquals(TextSize.SMALLEST, mSettings.getTextSize());
     }
 
+    @Test
     public void testAccessUseDoubleTree() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertFalse(mSettings.getUseDoubleTree());
 
         mSettings.setUseDoubleTree(true);
         assertFalse("setUseDoubleTree should be a no-op", mSettings.getUseDoubleTree());
     }
 
+    @Test
     public void testAccessUseWideViewPort() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertFalse(mSettings.getUseWideViewPort());
 
         mSettings.setUseWideViewPort(true);
         assertTrue(mSettings.getUseWideViewPort());
     }
 
+    @Test
     public void testSetNeedInitialFocus() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mSettings.setNeedInitialFocus(false);
 
         mSettings.setNeedInitialFocus(true);
     }
 
+    @Test
     public void testSetRenderPriority() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mSettings.setRenderPriority(WebSettings.RenderPriority.HIGH);
 
         mSettings.setRenderPriority(WebSettings.RenderPriority.LOW);
@@ -686,43 +665,35 @@
         mSettings.setRenderPriority(WebSettings.RenderPriority.NORMAL);
     }
 
+    @Test
     public void testAccessSupportMultipleWindows() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertFalse(mSettings.supportMultipleWindows());
 
         mSettings.setSupportMultipleWindows(true);
         assertTrue(mSettings.supportMultipleWindows());
     }
 
+    @Test
     public void testAccessSupportZoom() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertTrue(mSettings.supportZoom());
 
         mSettings.setSupportZoom(false);
         assertFalse(mSettings.supportZoom());
     }
 
+    @Test
     public void testAccessBuiltInZoomControls() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertFalse(mSettings.getBuiltInZoomControls());
 
         mSettings.setBuiltInZoomControls(true);
         assertTrue(mSettings.getBuiltInZoomControls());
     }
 
+    @Test
     public void testAppCacheDisabled() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Test that when AppCache is disabled, we don't get any AppCache
         // callbacks.
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String url = mWebServer.getAppCacheUrl();
         mSettings.setJavaScriptEnabled(true);
 
@@ -738,17 +709,15 @@
         assertEquals("Loaded", mOnUiThread.getTitle());
     }
 
+    @Test
     public void testAppCacheEnabled() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Note that the AppCache path can only be set once. This limits the
         // amount of testing we can do, and means that we must test all aspects
         // of setting the AppCache path in a single test to guarantee ordering.
 
         // Test that when AppCache is enabled but no valid path is provided,
         // we don't get any AppCache callbacks.
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String url = mWebServer.getAppCacheUrl();
         mSettings.setAppCacheEnabled(true);
         mSettings.setJavaScriptEnabled(true);
@@ -779,12 +748,10 @@
     // security exception in JS, most likely due to cross domain access. So we load
     // using a URL. Finally, it looks like enabling database requires creating a
     // webChromeClient and listening to Quota callbacks, which is not documented.
+    @Test
     public void testDatabaseDisabled() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Verify that websql database does not work when disabled.
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
         mOnUiThread.setWebChromeClient(new WebViewSyncLoader.WaitForProgressClient(mOnUiThread) {
             @Override
@@ -805,11 +772,8 @@
      * androidx.webkit.WebSettingsCompatTest#testDisabledActionModeMenuItems. Modifications to this
      * test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testDisabledActionModeMenuItems() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         assertEquals(WebSettings.MENU_ITEM_NONE, mSettings.getDisabledActionModeMenuItems());
 
         int allDisabledFlags = WebSettings.MENU_ITEM_NONE | WebSettings.MENU_ITEM_SHARE |
@@ -820,18 +784,14 @@
         }
     }
 
+    @Test
     public void testLoadsImagesAutomatically_default() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertTrue(mSettings.getLoadsImagesAutomatically());
     }
 
+    @Test
     public void testLoadsImagesAutomatically_httpImagesLoaded() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mSettings.setJavaScriptEnabled(true);
         mSettings.setLoadsImagesAutomatically(true);
 
@@ -839,11 +799,9 @@
         assertEquals(NETWORK_IMAGE_HEIGHT, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testLoadsImagesAutomatically_dataUriImagesLoaded() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mSettings.setJavaScriptEnabled(true);
         mSettings.setLoadsImagesAutomatically(true);
 
@@ -851,11 +809,9 @@
         assertEquals(DATA_URL_IMAGE_HEIGHT, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testLoadsImagesAutomatically_blockLoadingImages() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mSettings.setJavaScriptEnabled(true);
         mSettings.setLoadsImagesAutomatically(false);
 
@@ -867,11 +823,9 @@
         assertEquals(EMPTY_IMAGE_HEIGHT, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testLoadsImagesAutomatically_loadImagesWithoutReload() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mSettings.setJavaScriptEnabled(true);
         mSettings.setLoadsImagesAutomatically(false);
 
@@ -891,13 +845,11 @@
         assertEquals(DATA_URL_IMAGE_HEIGHT, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testBlockNetworkImage() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertFalse(mSettings.getBlockNetworkImage());
 
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mSettings.setJavaScriptEnabled(true);
 
         // Check that by default network and data url images are loaded.
@@ -923,13 +875,11 @@
         assertEquals(DATA_URL_IMAGE_HEIGHT, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testBlockNetworkLoads() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertFalse(mSettings.getBlockNetworkLoads());
 
-        startWebServer();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mSettings.setJavaScriptEnabled(true);
 
         // Check that by default network resources and data url images are loaded.
@@ -962,11 +912,8 @@
     }
 
     // Verify that an image in local file system can be loaded by an asset
+    @Test
     public void testLocalImageLoads() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mSettings.setJavaScriptEnabled(true);
         // Check that local images are loaded without issues regardless of domain checkings
         mSettings.setAllowUniversalAccessFromFileURLs(false);
@@ -979,11 +926,8 @@
 
     // Verify that javascript cross-domain request permissions matches file domain settings
     // for iframes
+    @Test
     public void testIframesWhenAccessFromFileURLsEnabled() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mSettings.setJavaScriptEnabled(true);
         // disable universal access from files
         mSettings.setAllowUniversalAccessFromFileURLs(false);
@@ -998,11 +942,8 @@
 
     // Verify that javascript cross-domain request permissions matches file domain settings
     // for iframes
+    @Test
     public void testIframesWhenAccessFromFileURLsDisabled() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mSettings.setJavaScriptEnabled(true);
         // disable universal access from files
         mSettings.setAllowUniversalAccessFromFileURLs(false);
@@ -1017,19 +958,14 @@
     }
 
     // Verify that enabling file access from file URLs enable XmlHttpRequest (XHR) across files
+    @Test
     public void testXHRWhenAccessFromFileURLsEnabled() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         verifyFileXHR(true);
     }
 
     // Verify that disabling file access from file URLs disable XmlHttpRequest (XHR) accross files
+    @Test
     public void testXHRWhenAccessFromFileURLsDisabled() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         verifyFileXHR(false);
     }
 
@@ -1077,11 +1013,8 @@
         fos.close();
     }
 
+    @Test
     public void testAllowMixedMode() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final String INSECURE_BASE_URL = "http://www.example.com/";
         final String INSECURE_JS_URL = INSECURE_BASE_URL + "insecure.js";
         final String INSECURE_IMG_URL = INSECURE_BASE_URL + "insecure.png";
@@ -1128,34 +1061,28 @@
         InterceptClient interceptClient = new InterceptClient();
         mOnUiThread.setWebViewClient(interceptClient);
         mSettings.setJavaScriptEnabled(true);
-        TestWebServer httpsServer = null;
-        try {
-            httpsServer = new TestWebServer(true);
-            String secureUrl = httpsServer.setResponse(SECURE_URL, SECURE_HTML, null);
-            mOnUiThread.clearSslPreferences();
 
-            mSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
-            mOnUiThread.loadUrlAndWaitForCompletion(secureUrl);
-            assertEquals(1, httpsServer.getRequestCount(SECURE_URL));
-            assertEquals(0, interceptClient.mInsecureJsCounter);
-            assertEquals(0, interceptClient.mInsecureImgCounter);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NO_CLIENT_AUTH);
+        String secureUrl = mWebServer.setResponse(SECURE_URL, SECURE_HTML, null);
+        mOnUiThread.clearSslPreferences();
 
-            mSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
-            mOnUiThread.loadUrlAndWaitForCompletion(secureUrl);
-            assertEquals(2, httpsServer.getRequestCount(SECURE_URL));
-            assertEquals(1, interceptClient.mInsecureJsCounter);
-            assertEquals(1, interceptClient.mInsecureImgCounter);
+        mSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
+        mOnUiThread.loadUrlAndWaitForCompletion(secureUrl);
+        assertEquals(1, mWebServer.getRequestCount(SECURE_URL));
+        assertEquals(0, interceptClient.mInsecureJsCounter);
+        assertEquals(0, interceptClient.mInsecureImgCounter);
 
-            mSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
-            mOnUiThread.loadUrlAndWaitForCompletion(secureUrl);
-            assertEquals(3, httpsServer.getRequestCount(SECURE_URL));
-            assertEquals(1, interceptClient.mInsecureJsCounter);
-            assertEquals(2, interceptClient.mInsecureImgCounter);
-        } finally {
-            if (httpsServer != null) {
-                httpsServer.shutdown();
-            }
-        }
+        mSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
+        mOnUiThread.loadUrlAndWaitForCompletion(secureUrl);
+        assertEquals(2, mWebServer.getRequestCount(SECURE_URL));
+        assertEquals(1, interceptClient.mInsecureJsCounter);
+        assertEquals(1, interceptClient.mInsecureImgCounter);
+
+        mSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
+        mOnUiThread.loadUrlAndWaitForCompletion(secureUrl);
+        assertEquals(3, mWebServer.getRequestCount(SECURE_URL));
+        assertEquals(1, interceptClient.mInsecureJsCounter);
+        assertEquals(2, interceptClient.mInsecureImgCounter);
     }
 
     /**
@@ -1163,10 +1090,8 @@
      * androidx.webkit.WebSettingsCompatTest#testEnableSafeBrowsing. Modifications to this test
      * should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testEnableSafeBrowsing() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertTrue("Safe Browsing should be enabled by default",
                 mSettings.getSafeBrowsingEnabled());
         mSettings.setSafeBrowsingEnabled(false);
@@ -1188,17 +1113,6 @@
     }
 
     /**
-     * Starts the internal web server. The server will be shut down automatically
-     * during tearDown().
-     *
-     * @throws Exception
-     */
-    private void startWebServer() throws Exception {
-        assertNull(mWebServer);
-        mWebServer = new CtsTestServer(getActivity(), false);
-    }
-
-    /**
      * Load the given asset from the internal web server. Starts the server if
      * it is not already running.
      *
@@ -1207,7 +1121,7 @@
      */
     private void loadAssetUrl(String asset) throws Exception {
         if (mWebServer == null) {
-            startWebServer();
+            mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         }
         String url = mWebServer.getAssetUrl(asset);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewClientTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewClientTest.java
index 928dcf8..cbe038c 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewClientTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewClientTest.java
@@ -16,19 +16,21 @@
 
 package android.webkit.cts;
 
-import android.app.ActivityManager;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
 import android.graphics.Bitmap;
-import android.os.Build;
 import android.os.Message;
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.view.KeyEvent;
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.webkit.HttpAuthHandler;
 import android.webkit.RenderProcessGoneDetail;
 import android.webkit.SafeBrowsingResponse;
-import android.webkit.ValueCallback;
 import android.webkit.WebChromeClient;
 import android.webkit.WebResourceError;
 import android.webkit.WebResourceRequest;
@@ -37,25 +39,42 @@
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
-import android.util.Pair;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
+
 import com.google.common.util.concurrent.SettableFuture;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.ByteArrayInputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.List;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 @AppModeFull
-public class WebViewClientTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewClientTest extends SharedWebViewTest {
     private static final String TEST_URL = "http://www.example.com/";
 
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
     private WebViewOnUiThread mOnUiThread;
-    private CtsTestServer mWebServer;
+    private SharedSdkWebServer mWebServer;
 
     private static final String TEST_SAFE_BROWSING_URL_PREFIX =
             "chrome://safe-browsing/match?type=";
@@ -68,36 +87,56 @@
     private static final String TEST_SAFE_BROWSING_BILLING_URL =
             TEST_SAFE_BROWSING_URL_PREFIX + "billing";
 
-    public WebViewClientTest() {
-        super("android.webkit.cts", WebViewCtsActivity.class);
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        final WebViewCtsActivity activity = getActivity();
-        WebView webview = activity.getWebView();
+    @Before
+    public void setUp() throws Exception {
+        WebView webview = getTestEnvironment().getWebView();
         if (webview != null) {
-            new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
-                @Override
-                    protected boolean check() {
-                    return activity.hasWindowFocus();
-                }
-            }.run();
-
             mOnUiThread = new WebViewOnUiThread(webview);
         }
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
         if (mWebServer != null) {
             mWebServer.shutdown();
         }
-        super.tearDown();
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                activity))
+                                    .setContext(activity)
+                                    .setWebView(webView)
+                                    .setRootLayout(((WebViewCtsActivity) activity).getRootLayout());
+                        });
+
+        SharedWebViewTestEnvironment environment = builder.build();
+
+        if (environment.getWebView() != null) {
+            new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
+                @Override
+                protected boolean check() {
+                    return ((WebViewCtsActivity) environment.getContext()).hasWindowFocus();
+                }
+            }.run();
+        }
+
+        return environment;
     }
 
     /**
@@ -107,10 +146,8 @@
      * http://go/modifying-webview-cts.
      */
     // Verify that the shouldoverrideurlloading is false by default
+    @Test
     public void testShouldOverrideUrlLoadingDefault() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final WebViewClient webViewClient = new WebViewClient();
         assertFalse(webViewClient.shouldOverrideUrlLoading(mOnUiThread.getWebView(), new String()));
     }
@@ -121,10 +158,8 @@
      * test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
     // Verify shouldoverrideurlloading called on top level navigation
+    @Test
     public void testShouldOverrideUrlLoading() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
@@ -142,11 +177,9 @@
 
     // Verify shouldoverrideurlloading called on webview called via onCreateWindow
     // TODO(sgurun) upstream this test to Aw.
+    @Test
     public void testShouldOverrideUrlLoadingOnCreateWindow() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         // WebViewClient for main window
         final MockWebViewClient mainWebViewClient = new MockWebViewClient();
         // WebViewClient for child window
@@ -168,7 +201,7 @@
                     childWebView.setWebViewClient(childWebViewClient);
                     childWebView.getSettings().setJavaScriptEnabled(true);
                     transport.setWebView(childWebView);
-                    getActivity().addContentView(childWebView, new ViewGroup.LayoutParams(
+                    getTestEnvironment().addContentView(childWebView, new ViewGroup.LayoutParams(
                                 ViewGroup.LayoutParams.FILL_PARENT,
                                 ViewGroup.LayoutParams.WRAP_CONTENT));
                     resultMsg.sendToTarget();
@@ -176,8 +209,8 @@
                 }
             });
             {
-                final int childCallCount =
-                        childWebViewClient.getShouldOverrideUrlLoadingCallCount();
+                final int childCallCount = childWebViewClient
+                        .getShouldOverrideUrlLoadingCallCount();
                 mOnUiThread.loadUrl(mWebServer.getAssetUrl(TestHtmlConstants.BLANK_TAG_URL));
 
                 new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
@@ -190,22 +223,21 @@
                     @Override
                     protected boolean check() {
                         return childWebViewClient
-                            .getShouldOverrideUrlLoadingCallCount() > childCallCount;
+                                .getShouldOverrideUrlLoadingCallCount() > childCallCount;
                     }
                 }.run();
                 assertEquals(mWebServer.getAssetUrl(TestHtmlConstants.PAGE_WITH_LINK_URL),
                         childWebViewClient.getLastShouldOverrideUrl());
             }
 
-            final int childCallCount =
-                    childWebViewClient.getShouldOverrideUrlLoadingCallCount();
+            final int childCallCount = childWebViewClient.getShouldOverrideUrlLoadingCallCount();
             final int mainCallCount = mainWebViewClient.getShouldOverrideUrlLoadingCallCount();
             clickOnLinkUsingJs("link", childWebViewOnUiThread);
             new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
                 @Override
                 protected boolean check() {
                     return childWebViewClient
-                        .getShouldOverrideUrlLoadingCallCount() > childCallCount;
+                            .getShouldOverrideUrlLoadingCallCount() > childCallCount;
                 }
             }.run();
             assertEquals(mainCallCount, mainWebViewClient.getShouldOverrideUrlLoadingCallCount());
@@ -213,6 +245,7 @@
             // also controlled by the test server)
             assertEquals(mWebServer.getAssetUrl(TestHtmlConstants.BLANK_PAGE_URL),
                     childWebViewClient.getLastShouldOverrideUrl());
+
         } finally {
             WebkitUtils.onMainThreadSync(() -> {
                 ViewParent parent = childWebView.getParent();
@@ -230,13 +263,11 @@
                         "console.log('element with id [" + linkId + "] clicked');"));
     }
 
+    @Test
     public void testLoadPage() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
-        mWebServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
 
         assertFalse(webViewClient.hasOnPageStartedCalled());
@@ -266,49 +297,41 @@
         }.run();
     }
 
+    @Test
     public void testOnReceivedLoginRequest() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
-        TestWebServer testServer = null;
+
         //set the url and html
         final String path = "/main";
         final String page = "<head></head><body>test onReceivedLoginRequest</body>";
         final String headerName = "x-auto-login";
         final String headerValue = "realm=com.google&account=foo%40bar.com&args=random_string";
-        List<Pair<String, String>> headers = new ArrayList<Pair<String, String>>();
-        headers.add(Pair.create(headerName, headerValue));
+        List<HttpHeader> headers = new ArrayList<HttpHeader>();
+        headers.add(HttpHeader.create(headerName, headerValue));
 
-        try {
-            testServer = new TestWebServer(false);
-            String url = testServer.setResponse(path, page, headers);
-            assertFalse(webViewClient.hasOnReceivedLoginRequest());
-            mOnUiThread.loadUrlAndWaitForCompletion(url);
-            assertTrue(webViewClient.hasOnReceivedLoginRequest());
-            new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
-                @Override
-                protected boolean check() {
-                    return webViewClient.hasOnReceivedLoginRequest();
-                }
-            }.run();
-           assertEquals("com.google", webViewClient.getLoginRequestRealm());
-           assertEquals("foo@bar.com", webViewClient.getLoginRequestAccount());
-           assertEquals("random_string", webViewClient.getLoginRequestArgs());
-        } finally {
-            testServer.shutdown();
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
+        String url = mWebServer.setResponse(path, page, headers);
+        assertFalse(webViewClient.hasOnReceivedLoginRequest());
+        mOnUiThread.loadUrlAndWaitForCompletion(url);
+        assertTrue(webViewClient.hasOnReceivedLoginRequest());
+        new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
+            @Override
+            protected boolean check() {
+                return webViewClient.hasOnReceivedLoginRequest();
+            }
+        }.run();
+        assertEquals("com.google", webViewClient.getLoginRequestRealm());
+        assertEquals("foo@bar.com", webViewClient.getLoginRequestAccount());
+        assertEquals("random_string", webViewClient.getLoginRequestArgs());
     }
     /**
      * This should remain functionally equivalent to
      * androidx.webkit.WebViewClientCompatTest#testOnReceivedError. Modifications to this test
      * should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testOnReceivedError() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
 
@@ -324,13 +347,12 @@
      * androidx.webkit.WebViewClientCompatTest#testOnReceivedErrorForSubresource. Modifications to
      * this test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testOnReceivedErrorForSubresource() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
+
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
-        mWebServer = new CtsTestServer(getActivity());
 
         assertNull(webViewClient.hasOnReceivedResourceError());
         String url = mWebServer.getAssetUrl(TestHtmlConstants.BAD_IMAGE_PAGE_URL);
@@ -340,13 +362,11 @@
                 webViewClient.hasOnReceivedResourceError().getErrorCode());
     }
 
+    @Test
     public void testOnReceivedHttpError() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
-        mWebServer = new CtsTestServer(getActivity());
 
         assertNull(webViewClient.hasOnReceivedHttpError());
         String url = mWebServer.getAssetUrl(TestHtmlConstants.NON_EXISTENT_PAGE_URL);
@@ -355,15 +375,13 @@
         assertEquals(404, webViewClient.hasOnReceivedHttpError().getStatusCode());
     }
 
+    @Test
     public void testOnFormResubmission() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
         final WebSettings settings = mOnUiThread.getSettings();
         settings.setJavaScriptEnabled(true);
-        mWebServer = new CtsTestServer(getActivity());
 
         assertFalse(webViewClient.hasOnFormResubmissionCalled());
         String url = mWebServer.getAssetUrl(TestHtmlConstants.JS_FORM_URL);
@@ -383,13 +401,11 @@
         }.run();
     }
 
+    @Test
     public void testDoUpdateVisitedHistory() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
-        mWebServer = new CtsTestServer(getActivity());
 
         assertFalse(webViewClient.hasDoUpdateVisitedHistoryCalled());
         String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
@@ -404,13 +420,11 @@
         }.run();
     }
 
+    @Test
     public void testOnReceivedHttpAuthRequest() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
-        mWebServer = new CtsTestServer(getActivity());
 
         assertFalse(webViewClient.hasOnReceivedHttpAuthRequestCalled());
         String url = mWebServer.getAuthAssetUrl(TestHtmlConstants.EMBEDDED_IMG_URL);
@@ -418,29 +432,25 @@
         assertTrue(webViewClient.hasOnReceivedHttpAuthRequestCalled());
     }
 
+    @Test
     public void testShouldOverrideKeyEvent() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
 
         assertFalse(webViewClient.shouldOverrideKeyEvent(mOnUiThread.getWebView(), null));
     }
 
+    @Test
     public void testOnUnhandledKeyEvent() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         requireLoadedPage();
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
 
         mOnUiThread.requestFocus();
-        getInstrumentation().waitForIdleSync();
+        getTestEnvironment().waitForIdleSync();
 
         assertFalse(webViewClient.hasOnUnhandledKeyEventCalled());
-        sendKeys(KeyEvent.KEYCODE_1);
+        getTestEnvironment().sendKeyDownUpSync(KeyEvent.KEYCODE_1);
 
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
@@ -450,13 +460,11 @@
         }.run();
     }
 
+    @Test
     public void testOnScaleChanged() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
-        mWebServer = new CtsTestServer(getActivity());
 
         assertFalse(webViewClient.hasOnScaleChangedCalled());
         String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
@@ -479,11 +487,8 @@
     }
 
     // Test that shouldInterceptRequest is called with the correct parameters
+    @Test
     public void testShouldInterceptRequestParams() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final String mainPath = "/main";
         final String mainPage = "<head></head><body>test page</body>";
         final String headerName = "x-test-header-name";
@@ -520,36 +525,26 @@
         TestClient client = new TestClient();
         mOnUiThread.setWebViewClient(client);
 
-        TestWebServer server = new TestWebServer(false);
-        try {
-            String mainUrl = server.setResponse(mainPath, mainPage, null);
-
-            mOnUiThread.loadUrlAndWaitForCompletion(mainUrl, headers);
-
-            // Inspect the fields of the saved WebResourceRequest
-            assertNotNull(client.interceptRequest);
-            assertEquals(mainUrl, client.interceptRequest.getUrl().toString());
-            assertTrue(client.interceptRequest.isForMainFrame());
-            assertEquals(server.getLastRequest(mainPath).getRequestLine().getMethod(),
-                client.interceptRequest.getMethod());
-
-            // Web request headers are case-insensitive. We provided lower-case headerName and
-            // headerValue. This will pass implementations which either do not mangle case,
-            // convert to lowercase, or convert to uppercase but return a case-insensitive map.
-            Map<String, String> interceptHeaders = client.interceptRequest.getRequestHeaders();
-            assertTrue(interceptHeaders.containsKey(headerName));
-            assertEquals(headerValue, interceptHeaders.get(headerName));
-        } finally {
-            server.shutdown();
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
+        String mainUrl = mWebServer.setResponse(mainPath, mainPage, null);
+        mOnUiThread.loadUrlAndWaitForCompletion(mainUrl, headers);
+        // Inspect the fields of the saved WebResourceRequest
+        assertNotNull(client.interceptRequest);
+        assertEquals(mainUrl, client.interceptRequest.getUrl().toString());
+        assertTrue(client.interceptRequest.isForMainFrame());
+        assertEquals(mWebServer.getLastRequest(mainPath).getMethod(),
+            client.interceptRequest.getMethod());
+        // Web request headers are case-insensitive. We provided lower-case headerName and
+        // headerValue. This will pass implementations which either do not mangle case,
+        // convert to lowercase, or convert to uppercase but return a case-insensitive map.
+        Map<String, String> interceptHeaders = client.interceptRequest.getRequestHeaders();
+        assertTrue(interceptHeaders.containsKey(headerName));
+        assertEquals(headerValue, interceptHeaders.get(headerName));
     }
 
     // Test that the WebResourceResponse returned by shouldInterceptRequest is handled correctly
+    @Test
     public void testShouldInterceptRequestResponse() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final String mainPath = "/main";
         final String mainPage = "<head></head><body>test page</body>";
         final String interceptPath = "/intercept_me";
@@ -579,56 +574,43 @@
         TestClient client = new TestClient();
         mOnUiThread.setWebViewClient(client);
 
-        TestWebServer server = new TestWebServer(false);
-        try {
-            String interceptUrl = server.getResponseUrl(interceptPath);
-
-            // JavaScript which makes a synchronous AJAX request and logs and returns the status
-            String js =
-                "(function() {" +
-                "  var xhr = new XMLHttpRequest();" +
-                "  xhr.open('GET', '" + interceptUrl + "', false);" +
-                "  xhr.send(null);" +
-                "  console.info('xhr.status = ' + xhr.status);" +
-                "  console.info('xhr.statusText = ' + xhr.statusText);" +
-                "  return '[' + xhr.status + '][' + xhr.statusText + ']';" +
-                "})();";
-
-            String mainUrl = server.setResponse(mainPath, mainPage, null);
-            mOnUiThread.loadUrlAndWaitForCompletion(mainUrl, null);
-
-            // Test a nonexistent page
-            client.interceptResponse = new WebResourceResponse("text/html", "UTF-8", null);
-            assertEquals("\"[404][Not Found]\"", mOnUiThread.evaluateJavascriptSync(js));
-
-            // Test an empty page
-            client.interceptResponse = new WebResourceResponse("text/html", "UTF-8",
-                new ByteArrayInputStream(new byte[0]));
-            assertEquals("\"[200][OK]\"", mOnUiThread.evaluateJavascriptSync(js));
-
-            // Test a nonempty page with unusual response code/text
-            client.interceptResponse =
-                new WebResourceResponse("text/html", "UTF-8", 123, "unusual", null,
-                    new ByteArrayInputStream("nonempty page".getBytes(StandardCharsets.UTF_8)));
-            assertEquals("\"[123][unusual]\"", mOnUiThread.evaluateJavascriptSync(js));
-        } finally {
-            server.shutdown();
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
+        String interceptUrl = mWebServer.getAbsoluteUrl(interceptPath);
+        // JavaScript which makes a synchronous AJAX request and logs and returns the status
+        String js =
+            "(function() {" +
+            "  var xhr = new XMLHttpRequest();" +
+            "  xhr.open('GET', '" + interceptUrl + "', false);" +
+            "  xhr.send(null);" +
+            "  console.info('xhr.status = ' + xhr.status);" +
+            "  console.info('xhr.statusText = ' + xhr.statusText);" +
+            "  return '[' + xhr.status + '][' + xhr.statusText + ']';" +
+            "})();";
+        String mainUrl = mWebServer.setResponse(mainPath, mainPage, null);
+        mOnUiThread.loadUrlAndWaitForCompletion(mainUrl, null);
+        // Test a nonexistent page
+        client.interceptResponse = new WebResourceResponse("text/html", "UTF-8", null);
+        assertEquals("\"[404][Not Found]\"", mOnUiThread.evaluateJavascriptSync(js));
+        // Test an empty page
+        client.interceptResponse = new WebResourceResponse("text/html", "UTF-8",
+            new ByteArrayInputStream(new byte[0]));
+        assertEquals("\"[200][OK]\"", mOnUiThread.evaluateJavascriptSync(js));
+        // Test a nonempty page with unusual response code/text
+        client.interceptResponse =
+            new WebResourceResponse("text/html", "UTF-8", 123, "unusual", null,
+                new ByteArrayInputStream("nonempty page".getBytes(StandardCharsets.UTF_8)));
+        assertEquals("\"[123][unusual]\"", mOnUiThread.evaluateJavascriptSync(js));
     }
 
     // Verify that OnRenderProcessGone returns false by default
+    @Test
     public void testOnRenderProcessGoneDefault() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final WebViewClient webViewClient = new WebViewClient();
         assertFalse(webViewClient.onRenderProcessGone(mOnUiThread.getWebView(), null));
     }
 
+    @Test
     public void testOnRenderProcessGone() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
         mOnUiThread.loadUrl("chrome://kill");
@@ -647,11 +629,9 @@
      * Modifications to this test should be reflected in that test as necessary. See
      * http://go/modifying-webview-cts.
      */
+    @Test
     public void testOnSafeBrowsingHitBackToSafety() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         final String ORIGINAL_URL = mOnUiThread.getUrl();
@@ -684,11 +664,9 @@
      * Modifications to this test should be reflected in that test as necessary. See
      * http://go/modifying-webview-cts.
      */
+    @Test
     public void testOnSafeBrowsingHitProceed() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         final String ORIGINAL_URL = mOnUiThread.getUrl();
@@ -712,10 +690,7 @@
 
     private void testOnSafeBrowsingCode(String expectedUrl, int expectedThreatType)
             throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         final String ORIGINAL_URL = mOnUiThread.getUrl();
@@ -743,6 +718,7 @@
      * Modifications to this test should be reflected in that test as necessary. See
      * http://go/modifying-webview-cts.
      */
+    @Test
     public void testOnSafeBrowsingMalwareCode() throws Throwable {
         testOnSafeBrowsingCode(TEST_SAFE_BROWSING_MALWARE_URL,
                 WebViewClient.SAFE_BROWSING_THREAT_MALWARE);
@@ -754,6 +730,7 @@
      * Modifications to this test should be reflected in that test as necessary. See
      * http://go/modifying-webview-cts.
      */
+    @Test
     public void testOnSafeBrowsingPhishingCode() throws Throwable {
         testOnSafeBrowsingCode(TEST_SAFE_BROWSING_PHISHING_URL,
                 WebViewClient.SAFE_BROWSING_THREAT_PHISHING);
@@ -765,6 +742,7 @@
      * Modifications to this test should be reflected in that test as necessary. See
      * http://go/modifying-webview-cts.
      */
+    @Test
     public void testOnSafeBrowsingUnwantedSoftwareCode() throws Throwable {
         testOnSafeBrowsingCode(TEST_SAFE_BROWSING_UNWANTED_SOFTWARE_URL,
                 WebViewClient.SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE);
@@ -776,15 +754,13 @@
      * Modifications to this test should be reflected in that test as necessary. See
      * http://go/modifying-webview-cts.
      */
+    @Test
     public void testOnSafeBrowsingBillingCode() throws Throwable {
         testOnSafeBrowsingCode(TEST_SAFE_BROWSING_BILLING_URL,
                 WebViewClient.SAFE_BROWSING_THREAT_BILLING);
     }
 
     private void requireLoadedPage() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mOnUiThread.loadUrlAndWaitForCompletion("about:blank");
     }
 
@@ -794,13 +770,10 @@
      * Modifications to this test should be reflected in that test as necessary. See
      * http://go/modifying-webview-cts.
      */
+    @Test
     public void testOnPageCommitVisibleCalled() throws Exception {
         // Check that the onPageCommitVisible callback is called
         // correctly.
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final SettableFuture<String> pageCommitVisibleFuture = SettableFuture.create();
         mOnUiThread.setWebViewClient(new WebViewClient() {
             public void onPageCommitVisible(WebView view, String url) {
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewCtsActivity.java b/tests/tests/webkit/src/android/webkit/cts/WebViewCtsActivity.java
index 953d2cb..8b64360 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewCtsActivity.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewCtsActivity.java
@@ -21,6 +21,7 @@
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.webkit.WebView;
+import android.widget.FrameLayout;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 
@@ -42,6 +43,10 @@
         return mWebView;
     }
 
+    public FrameLayout getRootLayout() {
+        return (FrameLayout) findViewById(android.R.id.content).getRootView();
+    }
+
     @Override
     public void onDestroy() {
         if (mWebView != null) {
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewDarkModeDarkThemeTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewDarkModeDarkThemeTest.java
index 9e81c14..e37d451 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewDarkModeDarkThemeTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewDarkModeDarkThemeTest.java
@@ -25,12 +25,14 @@
 
 import android.graphics.Color;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -41,6 +43,7 @@
 /**
  * Tests for {@link android.webkit.WebSettings#setAlgorithmicDarkeningAllowed(boolean)}
  */
+@MediumTest
 @RunWith(AndroidJUnit4.class)
 public class WebViewDarkModeDarkThemeTest extends WebViewDarkModeTestBase {
 
@@ -50,6 +53,7 @@
 
     @Before
     public void setUp() throws Exception {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
         init(mActivityRule.getActivity());
     }
 
@@ -61,10 +65,6 @@
 
     @Test
     public void testSimplifiedDarkMode_rendersDark() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         setWebViewSize(64, 64);
 
         // Set the webview non-focusable to avoid drawing the focus highlight.
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewDarkModeLightThemeTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewDarkModeLightThemeTest.java
index 4bab6da..7853898 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewDarkModeLightThemeTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewDarkModeLightThemeTest.java
@@ -24,12 +24,14 @@
 
 import android.graphics.Color;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -40,6 +42,7 @@
 /**
  * Tests for {@link android.webkit.WebSettings#setAlgorithmicDarkeningAllowed(boolean)}
  */
+@MediumTest
 @RunWith(AndroidJUnit4.class)
 public class WebViewDarkModeLightThemeTest extends WebViewDarkModeTestBase {
 
@@ -49,6 +52,7 @@
 
     @Before
     public void setUp() throws Exception {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
         init(mActivityRule.getActivity());
     }
 
@@ -60,10 +64,6 @@
 
     @Test
     public void testSimplifedDarkMode_rendersLight() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         setWebViewSize(64, 64);
 
         // Set the webview non-focusable to avoid drawing the focus highlight.
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewDataDirTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewDataDirTest.java
index 8e12c7a..f855fae 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewDataDirTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewDataDirTest.java
@@ -16,22 +16,48 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
 import android.content.Context;
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.CookieManager;
 import android.webkit.WebView;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 
-public class WebViewDataDirTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewDataDirTest {
+
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
 
     private static final String ALTERNATE_DIR_NAME = "test";
     private static final String COOKIE_URL = "https://www.webviewdatadirtest.com/";
     private static final String COOKIE_VALUE = "foo=main";
     private static final String SET_COOKIE_PARAMS = "; Max-Age=86400";
 
-    public WebViewDataDirTest() throws Exception {
-        super("android.webkit.cts", WebViewCtsActivity.class);
+    private WebViewCtsActivity mActivity;
+
+    @Before
+    public void setUp() throws Exception {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+        mActivityScenarioRule.getScenario().onActivity(activity -> {
+            mActivity = (WebViewCtsActivity) activity;
+        });
     }
 
     static class TestDisableThenUseImpl extends TestProcessClient.TestRunnable {
@@ -45,34 +71,25 @@
         }
     }
 
+    @Test
     public void testDisableThenUse() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        try (TestProcessClient process = TestProcessClient.createProcessA(getActivity())) {
+        try (TestProcessClient process = TestProcessClient.createProcessA(mActivity)) {
             process.run(TestDisableThenUseImpl.class);
         }
     }
 
+    @Test
     public void testUseThenDisable() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        assertNotNull(getActivity().getWebView());
+        assertNotNull(mActivity.getWebView());
         try {
             WebView.disableWebView();
             fail("didn't throw IllegalStateException");
         } catch (IllegalStateException e) {}
     }
 
+    @Test
     public void testUseThenChangeDir() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        assertNotNull(getActivity().getWebView());
+        assertNotNull(mActivity.getWebView());
         try {
             WebView.setDataDirectorySuffix(ALTERNATE_DIR_NAME);
             fail("didn't throw IllegalStateException");
@@ -89,12 +106,9 @@
         }
     }
 
+    @Test
     public void testInvalidDir() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        try (TestProcessClient process = TestProcessClient.createProcessA(getActivity())) {
+        try (TestProcessClient process = TestProcessClient.createProcessA(mActivity)) {
             process.run(TestInvalidDirImpl.class);
         }
     }
@@ -109,14 +123,11 @@
         }
     }
 
+    @Test
     public void testSameDirTwoProcesses() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        assertNotNull(mActivity.getWebView());
 
-        assertNotNull(getActivity().getWebView());
-
-        try (TestProcessClient processA = TestProcessClient.createProcessA(getActivity())) {
+        try (TestProcessClient processA = TestProcessClient.createProcessA(mActivity)) {
             processA.run(TestDefaultDirDisallowed.class);
         }
     }
@@ -131,18 +142,15 @@
         }
     }
 
+    @Test
     public void testCookieJarsSeparate() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         CookieManager cm = CookieManager.getInstance();
         cm.setCookie(COOKIE_URL, COOKIE_VALUE + SET_COOKIE_PARAMS);
         cm.flush();
         String cookie = cm.getCookie(COOKIE_URL);
         assertEquals("wrong cookie in default cookie jar", COOKIE_VALUE, cookie);
 
-        try (TestProcessClient processA = TestProcessClient.createProcessA(getActivity())) {
+        try (TestProcessClient processA = TestProcessClient.createProcessA(mActivity)) {
             processA.run(TestCookieInAlternateDir.class);
         }
     }
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewRenderProcessClientTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewRenderProcessClientTest.java
index c74dcb9..69deb36 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewRenderProcessClientTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewRenderProcessClientTest.java
@@ -16,46 +16,79 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.view.KeyEvent;
 import android.webkit.JavascriptInterface;
 import android.webkit.WebView;
 import android.webkit.WebViewRenderProcess;
 import android.webkit.WebViewRenderProcessClient;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 
 import com.google.common.util.concurrent.SettableFuture;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
 
 @AppModeFull
-public class WebViewRenderProcessClientTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewRenderProcessClientTest extends SharedWebViewTest {
     private WebViewOnUiThread mOnUiThread;
 
-    public WebViewRenderProcessClientTest() {
-        super("com.android.cts.webkit", WebViewCtsActivity.class);
-    }
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        final WebViewCtsActivity activity = getActivity();
-        WebView webView = activity.getWebView();
-        if (webView != null) {
-            mOnUiThread = new WebViewOnUiThread(webView);
+    @Before
+    public void setUp() throws Exception {
+        WebView webview = getTestEnvironment().getWebView();
+        if (webview != null) {
+            mOnUiThread = new WebViewOnUiThread(webview);
         }
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-        super.tearDown();
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                activity))
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
     }
 
     private static class JSBlocker {
@@ -151,18 +184,13 @@
         WebkitUtils.waitForFuture(rendererUnblocked);
     }
 
+    @Test
     public void testWebViewRenderProcessClientWithoutExecutor() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         testWebViewRenderProcessClientOnExecutor(null);
     }
 
+    @Test
     public void testWebViewRenderProcessClientWithExecutor() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final AtomicInteger executorCount = new AtomicInteger();
         testWebViewRenderProcessClientOnExecutor(new Executor() {
             @Override
@@ -174,11 +202,8 @@
         assertEquals(2, executorCount.get());
     }
 
+    @Test
     public void testSetWebViewRenderProcessClient() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         assertNull("Initially the renderer client should be null",
                 mOnUiThread.getWebViewRenderProcessClient());
 
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewRenderProcessTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewRenderProcessTest.java
index 941d970..5f5e118 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewRenderProcessTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewRenderProcessTest.java
@@ -16,44 +16,78 @@
 
 package android.webkit.cts;
 
-import android.annotation.SuppressLint;
-import android.os.Build;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.RenderProcessGoneDetail;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.webkit.WebViewRenderProcess;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
+
 import com.google.common.util.concurrent.SettableFuture;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.concurrent.Future;
 
 @AppModeFull
-public class WebViewRenderProcessTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewRenderProcessTest extends SharedWebViewTest {
     private WebViewOnUiThread mOnUiThread;
 
-    public WebViewRenderProcessTest() {
-        super("com.android.cts.webkit", WebViewCtsActivity.class);
-    }
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        final WebViewCtsActivity activity = getActivity();
-        WebView webView = activity.getWebView();
-        if (webView != null) {
-            mOnUiThread = new WebViewOnUiThread(webView);
+    @Before
+    public void setUp() throws Exception {
+        WebView webview = getTestEnvironment().getWebView();
+        if (webview != null) {
+            mOnUiThread = new WebViewOnUiThread(webview);
         }
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
-        super.tearDown();
+    }
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                    SharedWebViewTestEnvironment.createHostAppInvoker(
+                                            activity))
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
     }
 
     private boolean terminateRenderProcessOnUiThread(
@@ -106,11 +140,8 @@
         return future;
     }
 
+    @Test
     public void testGetWebViewRenderProcess() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final WebView webView = mOnUiThread.getWebView();
         final WebViewRenderProcess preStartRenderProcess = getRenderProcessOnUiThread(webView);
 
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewSslTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewSslTest.java
index 1796464..e47568d 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewSslTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewSslTest.java
@@ -16,46 +16,55 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 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 android.annotation.CallSuper;
-import android.net.Uri;
 import android.net.http.SslCertificate;
 import android.net.http.SslError;
 import android.os.StrictMode;
 import android.os.StrictMode.ThreadPolicy;
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.util.Log;
 import android.webkit.ClientCertRequest;
 import android.webkit.SslErrorHandler;
-import android.webkit.ValueCallback;
-import android.webkit.WebSettings;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
 
-import androidx.test.filters.FlakyTest;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.security.KeyFactory;
 import java.security.KeyStore;
-import java.security.PrivateKey;
 import java.security.Principal;
+import java.security.PrivateKey;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.Callable;
-
-import javax.net.ssl.X509TrustManager;
 
 @AppModeFull(reason = "Instant apps cannot bind sockets")
-public class WebViewSslTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewSslTest extends SharedWebViewTest {
     private static final String LOGTAG = "WebViewSslTest";
 
     /**
@@ -432,67 +441,84 @@
             (byte) 0x9c, (byte) 0xce, (byte) 0xa1, (byte) 0x87, (byte) 0x11, (byte) 0x51
     };
 
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
+    private WebViewCtsActivity mActivity;
+
     private WebView mWebView;
-    private CtsTestServer mWebServer;
+    private SharedSdkWebServer mWebServer;
     private WebViewOnUiThread mOnUiThread;
 
-    public WebViewSslTest() {
-        super("android.webkit.cts", WebViewCtsActivity.class);
+    @Before
+    public void setUp() throws Exception {
+        mWebView = getTestEnvironment().getWebView();
+        mOnUiThread = new WebViewOnUiThread(mWebView);
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        final WebViewCtsActivity activity = getActivity();
-        mWebView = activity.getWebView();
-        if (mWebView != null) {
-            new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
-                @Override
-                    protected boolean check() {
-                        return activity.hasWindowFocus();
-                }
-            }.run();
-            File f = activity.getFileStreamPath("snapshot");
-            if (f.exists()) {
-                f.delete();
-            }
-
-            mOnUiThread = new WebViewOnUiThread(mWebView);
-        }
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void cleanup() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
         if (mWebServer != null) {
-            stopWebServer();
+            stopWebServer(mWebServer);
         }
-        super.tearDown();
+        mActivity = null;
     }
 
-    private void startWebServer(boolean secure) throws Exception {
-        assertNull(mWebServer);
-        mWebServer = new CtsTestServer(getActivity(), secure);
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            mActivity = (WebViewCtsActivity) activity;
+
+                            WebView webView = mActivity.getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                    mActivity))
+                                    .setContext(mActivity)
+                                    .setWebView(webView);
+                        });
+
+        SharedWebViewTestEnvironment environment = builder.build();
+
+        if (environment.getWebView() != null) {
+            new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
+                @Override
+                protected boolean check() {
+                    return mActivity.hasWindowFocus();
+                }
+            }.run();
+            File f = mActivity.getFileStreamPath("snapshot");
+            if (f.exists()) {
+                f.delete();
+            }
+        }
+        return environment;
     }
 
-    private void stopWebServer() throws Exception {
-        assertNotNull(mWebServer);
+    private void stopWebServer(SharedSdkWebServer webServer) throws Exception {
+        assertNotNull(webServer);
         ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
         ThreadPolicy tmpPolicy = new ThreadPolicy.Builder(oldPolicy)
                 .permitNetwork()
                 .build();
         StrictMode.setThreadPolicy(tmpPolicy);
-        mWebServer.shutdown();
-        mWebServer = null;
+        webServer.shutdown();
+        webServer = null;
         StrictMode.setThreadPolicy(oldPolicy);
     }
 
+    @Test
     public void testInsecureSiteClearsCertificate() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final class MockWebViewClient extends WaitForLoadedClient {
             public MockWebViewClient() {
                 super(mOnUiThread);
@@ -503,7 +529,7 @@
             }
         }
 
-        startWebServer(true);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NO_CLIENT_AUTH);
         mOnUiThread.setWebViewClient(new MockWebViewClient());
         mOnUiThread.loadUrlAndWaitForCompletion(
                 mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL));
@@ -511,18 +537,18 @@
         assertNotNull(cert);
         assertEquals("Android", cert.getIssuedTo().getUName());
 
-        stopWebServer();
+        stopWebServer(mWebServer);
 
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         mOnUiThread.loadUrlAndWaitForCompletion(
                 mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL));
         assertNull(mOnUiThread.getCertificate());
     }
 
+    @Test
     public void testSecureSiteSetsCertificate() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        SharedWebViewTestEnvironment testEnvironment = getTestEnvironment();
+
         final class MockWebViewClient extends WaitForLoadedClient {
             public MockWebViewClient() {
                 super(mOnUiThread);
@@ -533,14 +559,14 @@
             }
         }
 
-        startWebServer(false);
+        mWebServer = testEnvironment.getSetupWebServer(SslMode.INSECURE);
         mOnUiThread.loadUrlAndWaitForCompletion(
                 mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL));
         assertNull(mOnUiThread.getCertificate());
 
-        stopWebServer();
+        stopWebServer(mWebServer);
 
-        startWebServer(true);
+        mWebServer = testEnvironment.getSetupWebServer(SslMode.NO_CLIENT_AUTH);
         mOnUiThread.setWebViewClient(new MockWebViewClient());
         mOnUiThread.loadUrlAndWaitForCompletion(
                 mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL));
@@ -549,14 +575,12 @@
         assertEquals("Android", cert.getIssuedTo().getUName());
     }
 
+    @Test
     public void testClearSslPreferences() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Load the first page. We expect a call to
         // WebViewClient.onReceivedSslError().
         final SslErrorWebViewClient webViewClient = new SslErrorWebViewClient(mOnUiThread);
-        startWebServer(true);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NO_CLIENT_AUTH);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         mOnUiThread.setWebViewClient(webViewClient);
         mOnUiThread.clearSslPreferences();
@@ -582,10 +606,8 @@
         assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testOnReceivedSslError() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final class MockWebViewClient extends WaitForLoadedClient {
             private String mErrorUrl;
             private WebView mWebView;
@@ -607,7 +629,7 @@
             }
         }
 
-        startWebServer(true);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NO_CLIENT_AUTH);
         final String errorUrl = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         final MockWebViewClient webViewClient = new MockWebViewClient();
         mOnUiThread.setWebViewClient(webViewClient);
@@ -618,10 +640,8 @@
         assertEquals(errorUrl, webViewClient.errorUrl());
     }
 
+    @Test
     public void testOnReceivedSslErrorProceed() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final class MockWebViewClient extends WaitForLoadedClient {
             public MockWebViewClient() {
                 super(mOnUiThread);
@@ -632,17 +652,15 @@
             }
         }
 
-        startWebServer(true);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NO_CLIENT_AUTH);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         mOnUiThread.setWebViewClient(new MockWebViewClient());
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testOnReceivedSslErrorCancel() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final class MockWebViewClient extends WaitForLoadedClient {
             public MockWebViewClient() {
                 super(mOnUiThread);
@@ -653,7 +671,7 @@
             }
         }
 
-        startWebServer(true);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NO_CLIENT_AUTH);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         mOnUiThread.setWebViewClient(new MockWebViewClient());
         mOnUiThread.clearSslPreferences();
@@ -661,14 +679,12 @@
         assertNotEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testSslErrorProceedResponseReusedForSameHost() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Load the first page. We expect a call to
         // WebViewClient.onReceivedSslError().
         final SslErrorWebViewClient webViewClient = new SslErrorWebViewClient(mOnUiThread);
-        startWebServer(true);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NO_CLIENT_AUTH);
         final String firstUrl = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
         mOnUiThread.setWebViewClient(webViewClient);
         mOnUiThread.clearSslPreferences();
@@ -686,14 +702,12 @@
         assertEquals("Second page", mOnUiThread.getTitle());
     }
 
+    @Test
     public void testSslErrorProceedResponseNotReusedForDifferentHost() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Load the first page. We expect a call to
         // WebViewClient.onReceivedSslError().
         final SslErrorWebViewClient webViewClient = new SslErrorWebViewClient(mOnUiThread);
-        startWebServer(true);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NO_CLIENT_AUTH);
         final String firstUrl = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
         mOnUiThread.setWebViewClient(webViewClient);
         mOnUiThread.clearSslPreferences();
@@ -714,11 +728,9 @@
         assertEquals("Second page", mOnUiThread.getTitle());
     }
 
+    @Test
     public void testSecureServerRequestingClientCertDoesNotCancelRequest() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.WANTS_CLIENT_AUTH);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.WANTS_CLIENT_AUTH);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         final SslErrorWebViewClient webViewClient = new SslErrorWebViewClient(mOnUiThread);
         mOnUiThread.setWebViewClient(webViewClient);
@@ -731,11 +743,9 @@
         assertEquals(0, webViewClient.onReceivedErrorCode());
     }
 
+    @Test
     public void testSecureServerRequiringClientCertDoesCancelRequest() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.NEEDS_CLIENT_AUTH);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NEEDS_CLIENT_AUTH);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         final SslErrorWebViewClient webViewClient = new SslErrorWebViewClient(mOnUiThread);
         mOnUiThread.setWebViewClient(webViewClient);
@@ -759,11 +769,9 @@
                 TestHtmlConstants.HELLO_WORLD_TITLE.equals(mOnUiThread.getTitle()));
     }
 
+    @Test
     public void testProceedClientCertRequest() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.NEEDS_CLIENT_AUTH);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NEEDS_CLIENT_AUTH);
         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         final ClientCertWebViewClient webViewClient = new ClientCertWebViewClient(mOnUiThread);
         mOnUiThread.setWebViewClient(webViewClient);
@@ -789,11 +797,9 @@
                 1, webViewClient.getClientCertRequestCount());
     }
 
+    @Test
     public void testProceedClientCertRequestKeyWithAndroidKeystoreKey() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.NEEDS_CLIENT_AUTH);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NEEDS_CLIENT_AUTH);
         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         final ClientCertWebViewClient webViewClient = new ClientCertWebViewClient(
                 mOnUiThread,
@@ -842,11 +848,9 @@
                 "Reached max number of tries and never saw error " + expectedErrorCode);
     }
 
+    @Test
     public void testIgnoreClientCertRequest() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.NEEDS_CLIENT_AUTH);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NEEDS_CLIENT_AUTH);
         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         final ClientCertWebViewClient webViewClient = new ClientCertWebViewClient(mOnUiThread);
         mOnUiThread.setWebViewClient(webViewClient);
@@ -878,11 +882,9 @@
         assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testCancelClientCertRequest() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.NEEDS_CLIENT_AUTH);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NEEDS_CLIENT_AUTH);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         final ClientCertWebViewClient webViewClient = new ClientCertWebViewClient(mOnUiThread);
         mOnUiThread.setWebViewClient(webViewClient);
@@ -905,38 +907,10 @@
         assertNotEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mOnUiThread.getTitle());
     }
 
-    /**
-     * {@link X509TrustManager} that trusts everybody.
-     */
-    private static class TrustManager implements X509TrustManager {
-        public void checkClientTrusted(X509Certificate[] chain, String authType) {
-            // Trust the CtSTestServer's client...
-        }
-
-        public void checkServerTrusted(X509Certificate[] chain, String authType) {
-            // Trust the CtSTestServer...
-        }
-
-        public X509Certificate[] getAcceptedIssuers() {
-            try {
-                CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
-                return new X509Certificate[] {
-                        (X509Certificate) certFactory.generateCertificate(
-                                new ByteArrayInputStream(FAKE_RSA_CA_1))
-                        };
-            } catch (Exception ex) {
-                Log.e(LOGTAG, "failed creating certificate chain" + ex);
-                return null;
-            }
-        }
-    }
-
+    @Test
     public void testClientCertIssuersReceivedCorrectly() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.NEEDS_CLIENT_AUTH,
-                new TrustManager());
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.NEEDS_CLIENT_AUTH,
+            FAKE_RSA_CA_1, 0, 0);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         final ClientCertWebViewClient webViewClient = new ClientCertWebViewClient(mOnUiThread);
         mOnUiThread.setWebViewClient(webViewClient);
@@ -1092,7 +1066,7 @@
                     request.proceed(key, certChain);
                     return;
                 } catch (Exception e) {
-                    Log.e(LOGTAG,  "Fatal error" + e);
+                    throw new RuntimeException(e);
                 }
             }
             throw new IllegalStateException("unknown action");
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewStartupTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewStartupTest.java
index 7ca217b..651ee7e 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewStartupTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewStartupTest.java
@@ -16,21 +16,30 @@
 
 package android.webkit.cts;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
-import android.test.InstrumentationTestCase;
 import android.webkit.WebView;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 
 import com.google.common.util.concurrent.SettableFuture;
 
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 /**
  * Test class testing different aspects of WebView loading.
  *
@@ -41,7 +50,9 @@
  * <p>Tests in this class are moved from {@link com.android.cts.webkit.WebViewHostSideStartupTest},
  * see http://b/72376996 for the migration of these tests.
  */
-public class WebViewStartupTest extends InstrumentationTestCase {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewStartupTest {
     private static final String TEST_PROCESS_DATA_DIR_SUFFIX = "WebViewStartupTestDir";
     private static final long TEST_TIMEOUT_MS = 3000;
 
@@ -94,7 +105,10 @@
         }
     }
 
+    @Test
     public void testGetCurrentWebViewPackageOnUiThread() throws Throwable {
+        // runCurrentWebViewPackageTest handles the case where WebView is not supported on device,
+        // so we don't need to check NullWebViewUtils.
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         try (TestProcessClient process = TestProcessClient.createProcessA(context)) {
             process.run(TestGetCurrentWebViewPackageOnUiThread.class);
@@ -109,7 +123,10 @@
         }
     }
 
+    @Test
     public void testGetCurrentWebViewPackageOnBackgroundThread() throws Throwable {
+        // runCurrentWebViewPackageTest handles the case where WebView is not supported on device,
+        // so we don't need to check NullWebViewUtils.
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         try (TestProcessClient process = TestProcessClient.createProcessA(context)) {
             process.run(TestGetCurrentWebViewPackageOnBackgroundThread.class);
@@ -128,10 +145,9 @@
         }
     }
 
+    @Test
     public void testGetWebViewLooperOnUiThread() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
 
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         try (TestProcessClient process = TestProcessClient.createProcessA(context)) {
@@ -160,10 +176,9 @@
      * Ensure that a WebView created on the UI thread returns that thread as its creator thread.
      * This ensures WebView.getWebViewLooper() is not implemented as 'return Looper.myLooper();'.
      */
+    @Test
     public void testGetWebViewLooperCreatedOnUiThreadFromInstrThread() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
 
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         try (TestProcessClient process = TestProcessClient.createProcessA(context)) {
@@ -203,11 +218,10 @@
      * thread. This ensures WebView.getWebViewLooper() is not bound to the UI thread regardless of
      * the thread it is created on..
      */
+    @Test
     public void testGetWebViewLooperCreatedOnBackgroundThreadFromInstThread()
             throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
 
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
         try (TestProcessClient process = TestProcessClient.createProcessA(context)) {
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java
index 26c882f..1b39b27 100755
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java
@@ -19,7 +19,9 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.*;
 
+import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -38,8 +40,6 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
-import android.os.StrictMode;
-import android.os.StrictMode.ThreadPolicy;
 import android.os.SystemClock;
 import android.platform.test.annotations.AppModeFull;
 import android.platform.test.annotations.Presubmit;
@@ -49,18 +49,13 @@
 import android.print.PrintDocumentAdapter.LayoutResultCallback;
 import android.print.PrintDocumentAdapter.WriteResultCallback;
 import android.print.PrintDocumentInfo;
-import android.test.ActivityInstrumentationTestCase2;
-import android.test.UiThreadTest;
-import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.ViewGroup;
 import android.view.textclassifier.TextClassification;
 import android.view.textclassifier.TextClassifier;
 import android.view.textclassifier.TextSelection;
-import android.webkit.ConsoleMessage;
 import android.webkit.CookieSyncManager;
 import android.webkit.DownloadListener;
 import android.webkit.JavascriptInterface;
@@ -81,47 +76,47 @@
 import android.webkit.cts.WebViewSyncLoader.WaitForProgressClient;
 import android.widget.LinearLayout;
 
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.FlakyTest;
+import androidx.test.filters.MediumTest;
 
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
+
 import com.google.common.util.concurrent.SettableFuture;
 
-import java.io.ByteArrayInputStream;
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
-
 import java.net.MalformedURLException;
 import java.net.URL;
-
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
-
-import java.util.Collections;
-import java.util.Date;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpEntityEnclosingRequest;
-import org.apache.http.HttpRequest;
-import org.apache.http.util.EncodingUtils;
-import org.apache.http.util.EntityUtils;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 @AppModeFull
-public class WebViewTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewTest extends SharedWebViewTest {
     private static final int INITIAL_PROGRESS = 100;
     private static final String X_REQUESTED_WITH = "X-Requested-With";
     private static final String PRINTER_TEST_FILE = "print.pdf";
@@ -132,378 +127,377 @@
     private static final String SIMPLE_HTML = "<html><body>simple html</body></html>";
 
     /**
-     * This is the minimum number of milliseconds to wait for scrolling to
-     * start. If no scrolling has started before this timeout then it is
-     * assumed that no scrolling will happen.
+     * This is the minimum number of milliseconds to wait for scrolling to start. If no scrolling
+     * has started before this timeout then it is assumed that no scrolling will happen.
      */
     private static final long MIN_SCROLL_WAIT_MS = 1000;
 
     /**
-     * This is the minimum number of milliseconds to wait for findAll to
-     * find all the matches. If matches are not found, the Listener would
-     * call findAll again until it times out.
+     * This is the minimum number of milliseconds to wait for findAll to find all the matches. If
+     * matches are not found, the Listener would call findAll again until it times out.
      */
     private static final long MIN_FIND_WAIT_MS = 3000;
 
     /**
-     * Once scrolling has started, this is the interval that scrolling
-     * is checked to see if there is a change. If no scrolling change
-     * has happened in the given time then it is assumed that scrolling
-     * has stopped.
+     * Once scrolling has started, this is the interval that scrolling is checked to see if there is
+     * a change. If no scrolling change has happened in the given time then it is assumed that
+     * scrolling has stopped.
      */
     private static final long SCROLL_WAIT_INTERVAL_MS = 200;
 
-    private WebView mWebView;
-    private CtsTestServer mWebServer;
-    private WebViewOnUiThread mOnUiThread;
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
+    private Context mContext;
+    private SharedSdkWebServer mWebServer;
     private WebIconDatabase mIconDb;
+    private WebView mWebView;
+    private WebViewOnUiThread mOnUiThread;
 
-    public WebViewTest() {
-        super("com.android.cts.webkit", WebViewCtsActivity.class);
+    @Before
+    public void setUp() throws Exception {
+        mWebView = getTestEnvironment().getWebView();
+        mOnUiThread = new WebViewOnUiThread(mWebView);
+        mContext = getTestEnvironment().getContext();
     }
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        final WebViewCtsActivity activity = getActivity();
-        mWebView = activity.getWebView();
-        if (mWebView != null) {
-            new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
-                @Override
-                    protected boolean check() {
-                        return activity.hasWindowFocus();
-                }
-            }.run();
-            File f = activity.getFileStreamPath("snapshot");
-            if (f.exists()) {
-                f.delete();
-            }
-
-            mOnUiThread = new WebViewOnUiThread(mWebView);
-        }
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void cleanup() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
         if (mWebServer != null) {
-            stopWebServer();
+            mWebServer.shutdown();
         }
         if (mIconDb != null) {
             mIconDb.removeAllIcons();
             mIconDb.close();
             mIconDb = null;
         }
-        super.tearDown();
     }
 
-    private void startWebServer(boolean secure) throws Exception {
-        assertNull(mWebServer);
-        mWebServer = new CtsTestServer(getActivity(), secure);
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                    activity))
+                                    .setContext(activity)
+                                    .setWebView(webView)
+                                    .setRootLayout(((WebViewCtsActivity) activity).getRootLayout());
+                        });
+
+        SharedWebViewTestEnvironment environment = builder.build();
+
+        // Wait for window focus and clean up the snapshot before
+        // returning the test environment.
+        if (environment.getWebView() != null) {
+            new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
+                @Override
+                protected boolean check() {
+                    return ((Activity) environment.getContext()).hasWindowFocus();
+                }
+            }.run();
+            File f = environment.getContext().getFileStreamPath("snapshot");
+            if (f.exists()) {
+                f.delete();
+            }
+        }
+
+        return environment;
     }
 
-    private void stopWebServer() throws Exception {
-        assertNotNull(mWebServer);
-        ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
-        ThreadPolicy tmpPolicy = new ThreadPolicy.Builder(oldPolicy)
-                .permitNetwork()
-                .build();
-        StrictMode.setThreadPolicy(tmpPolicy);
-        mWebServer.shutdown();
-        mWebServer = null;
-        StrictMode.setThreadPolicy(oldPolicy);
-    }
-
-    @UiThreadTest
+    @Test
     public void testConstructor() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        WebView webView = new WebView(getActivity());
-        webView.destroy();
-        webView = new WebView(getActivity(), null);
-        webView.destroy();
-        webView = new WebView(getActivity(), null, 0);
-        webView.destroy();
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    WebView webView = new WebView(mContext);
+                    webView.destroy();
+                    webView = new WebView(mContext, null);
+                    webView.destroy();
+                    webView = new WebView(mContext, null, 0);
+                    webView.destroy();
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testCreatingWebViewWithDeviceEncrpytionFails() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        Context deviceEncryptedContext = getActivity().createDeviceProtectedStorageContext();
-        try {
-            new WebView(deviceEncryptedContext);
-            fail("WebView should have thrown exception when creating with a device " +
-                "protected storage context");
-        } catch (IllegalArgumentException e) {}
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    Context deviceEncryptedContext = mContext.createDeviceProtectedStorageContext();
+                    try {
+                        new WebView(deviceEncryptedContext);
+                        fail(
+                                "WebView should have thrown exception when creating with a device "
+                                        + "protected storage context");
+                    } catch (IllegalArgumentException e) {
+                    }
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testCreatingWebViewWithMultipleEncryptionContext() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    // Credential encryption is the default. Create one here for the sake of
+                    // clarity.
+                    Context credentialEncryptedContext =
+                            mContext.createCredentialProtectedStorageContext();
+                    Context deviceEncryptedContext = mContext.createDeviceProtectedStorageContext();
 
-        // Credential encrpytion is the default. Create one here for the sake of clarity.
-        Context credentialEncryptedContext = getActivity().createCredentialProtectedStorageContext();
-        Context deviceEncryptedContext = getActivity().createDeviceProtectedStorageContext();
+                    // No exception should be thrown with credential encryption context.
+                    WebView webView = new WebView(credentialEncryptedContext);
+                    webView.destroy();
 
-        // No exception should be thrown with credential encryption context.
-        WebView webView = new WebView(credentialEncryptedContext);
-        webView.destroy();
-
-        try {
-            new WebView(deviceEncryptedContext);
-            fail("WebView should have thrown exception when creating with a device " +
-                "protected storage context");
-        } catch (IllegalArgumentException e) {}
+                    try {
+                        new WebView(deviceEncryptedContext);
+                        fail(
+                                "WebView should have thrown exception when creating with a device "
+                                        + "protected storage context");
+                    } catch (IllegalArgumentException e) {
+                    }
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testCreatingWebViewCreatesCookieSyncManager() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        WebView webView = new WebView(getActivity());
-        assertNotNull(CookieSyncManager.getInstance());
-        webView.destroy();
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    WebView webView = new WebView(mContext);
+                    assertNotNull(CookieSyncManager.getInstance());
+                    webView.destroy();
+                });
     }
 
+    @Test
     // Static methods should be safe to call on non-UI threads
     public void testFindAddress() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         /*
          * Info about USPS
          * http://en.wikipedia.org/wiki/Postal_address#United_States
          * http://www.usps.com/
          */
         // full address
-        assertEquals("455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA 92826",
+        assertEquals(
+                "455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA 92826",
                 WebView.findAddress("455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA 92826"));
         // Zipcode is optional.
-        assertEquals("455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA",
+        assertEquals(
+                "455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA",
                 WebView.findAddress("455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA"));
         // not an address
         assertNull(WebView.findAddress("This is not an address: no town, no state, no zip."));
 
         // would be an address, except for numbers that are not ASCII
-        assertNull(WebView.findAddress(
-                "80\uD835\uDFEF \uD835\uDFEF\uD835\uDFEFth Avenue Sunnyvale, CA 94089"));
+        assertNull(
+                WebView.findAddress(
+                        "80\uD835\uDFEF \uD835\uDFEF\uD835\uDFEFth Avenue Sunnyvale, CA 94089"));
     }
 
-    @UiThreadTest
+    @Test
     public void testScrollBarOverlay() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    // These functions have no effect; just verify they don't crash
+                    mWebView.setHorizontalScrollbarOverlay(true);
+                    mWebView.setVerticalScrollbarOverlay(false);
 
-        // These functions have no effect; just verify they don't crash
-        mWebView.setHorizontalScrollbarOverlay(true);
-        mWebView.setVerticalScrollbarOverlay(false);
-
-        assertTrue(mWebView.overlayHorizontalScrollbar());
-        assertFalse(mWebView.overlayVerticalScrollbar());
+                    assertTrue(mWebView.overlayHorizontalScrollbar());
+                    assertFalse(mWebView.overlayVerticalScrollbar());
+                });
     }
 
+    @Test
     @Presubmit
-    @UiThreadTest
     public void testLoadUrl() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
-        assertNull(mWebView.getUrl());
-        assertNull(mWebView.getOriginalUrl());
-        assertEquals(INITIAL_PROGRESS, mWebView.getProgress());
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    assertNull(mWebView.getUrl());
+                    assertNull(mWebView.getOriginalUrl());
+                    assertEquals(INITIAL_PROGRESS, mWebView.getProgress());
 
-        startWebServer(false);
-        String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
-        mOnUiThread.loadUrlAndWaitForCompletion(url);
-        assertEquals(100, mWebView.getProgress());
-        assertEquals(url, mWebView.getUrl());
-        assertEquals(url, mWebView.getOriginalUrl());
-        assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mWebView.getTitle());
+                    String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url);
+                    assertEquals(100, mWebView.getProgress());
+                    assertEquals(url, mWebView.getUrl());
+                    assertEquals(url, mWebView.getOriginalUrl());
+                    assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mWebView.getTitle());
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testPostUrlWithNonNetworkUrl() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final String nonNetworkUrl = "file:///android_asset/" + TestHtmlConstants.HELLO_WORLD_URL;
 
         mOnUiThread.postUrlAndWaitForCompletion(nonNetworkUrl, new byte[1]);
 
-        assertEquals("Non-network URL should have loaded", TestHtmlConstants.HELLO_WORLD_TITLE,
-                mWebView.getTitle());
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    assertEquals(
+                            "Non-network URL should have loaded",
+                            TestHtmlConstants.HELLO_WORLD_TITLE,
+                            mWebView.getTitle());
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testPostUrlWithNetworkUrl() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
+
         final String networkUrl = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         final String postDataString = "username=my_username&password=my_password";
-        final byte[] postData = EncodingUtils.getBytes(postDataString, "BASE64");
+        final byte[] postData = getTestEnvironment().getEncodingBytes(postDataString, "BASE64");
 
         mOnUiThread.postUrlAndWaitForCompletion(networkUrl, postData);
 
-        HttpRequest request = mWebServer.getLastRequest(TestHtmlConstants.HELLO_WORLD_URL);
-        assertEquals("The last request should be POST", request.getRequestLine().getMethod(),
-                "POST");
-
-        assertTrue("The last request should have a request body",
-                request instanceof HttpEntityEnclosingRequest);
-        HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
-        String entityString = EntityUtils.toString(entity);
-        assertEquals(entityString, postDataString);
+        HttpRequest request = mWebServer.getLastAssetRequest(TestHtmlConstants.HELLO_WORLD_URL);
+        assertEquals("The last request should be POST", request.getMethod(), "POST");
+        assertEquals(request.getBody(), postDataString);
     }
 
-    @UiThreadTest
+    @Test
     public void testLoadUrlDoesNotStripParamsWhenLoadingContentUrls() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        Uri.Builder uriBuilder = new Uri.Builder().scheme(
-                ContentResolver.SCHEME_CONTENT).authority(MockContentProvider.AUTHORITY);
-        uriBuilder.appendPath("foo.html").appendQueryParameter("param","bar");
-        String url = uriBuilder.build().toString();
-        mOnUiThread.loadUrlAndWaitForCompletion(url);
-        // verify the parameter is not stripped.
-        Uri uri = Uri.parse(mWebView.getTitle());
-        assertEquals("bar", uri.getQueryParameter("param"));
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    Uri.Builder uriBuilder =
+                            new Uri.Builder()
+                                    .scheme(ContentResolver.SCHEME_CONTENT)
+                                    .authority(MockContentProvider.AUTHORITY);
+                    uriBuilder.appendPath("foo.html").appendQueryParameter("param", "bar");
+                    String url = uriBuilder.build().toString();
+                    mOnUiThread.loadUrlAndWaitForCompletion(url);
+                    // verify the parameter is not stripped.
+                    Uri uri = Uri.parse(mWebView.getTitle());
+                    assertEquals("bar", uri.getQueryParameter("param"));
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testAppInjectedXRequestedWithHeaderIsNotOverwritten() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
-        startWebServer(false);
-        String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
-        HashMap<String, String> map = new HashMap<String, String>();
-        final String requester = "foo";
-        map.put(X_REQUESTED_WITH, requester);
-        mOnUiThread.loadUrlAndWaitForCompletion(url, map);
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
+                    HashMap<String, String> map = new HashMap<String, String>();
+                    final String requester = "foo";
+                    map.put(X_REQUESTED_WITH, requester);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url, map);
 
-        // verify that the request also includes X-Requested-With header
-        // but is not overwritten by the webview
-        HttpRequest request = mWebServer.getLastRequest(TestHtmlConstants.HELLO_WORLD_URL);
-        Header[] matchingHeaders = request.getHeaders(X_REQUESTED_WITH);
-        assertEquals(1, matchingHeaders.length);
+                    // verify that the request also includes X-Requested-With header
+                    // but is not overwritten by the webview
+                    HttpRequest request =
+                            mWebServer.getLastAssetRequest(TestHtmlConstants.HELLO_WORLD_URL);
+                    String[] matchingHeaders = request.getHeaders(X_REQUESTED_WITH);
+                    assertEquals(1, matchingHeaders.length);
 
-        Header header = matchingHeaders[0];
-        assertEquals(requester, header.getValue());
+                    String header = matchingHeaders[0];
+                    assertEquals(requester, header);
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testAppCanInjectHeadersViaImmutableMap() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
-        startWebServer(false);
-        String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
-        HashMap<String, String> map = new HashMap<String, String>();
-        final String requester = "foo";
-        map.put(X_REQUESTED_WITH, requester);
-        mOnUiThread.loadUrlAndWaitForCompletion(url, Collections.unmodifiableMap(map));
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
+                    HashMap<String, String> map = new HashMap<String, String>();
+                    final String requester = "foo";
+                    map.put(X_REQUESTED_WITH, requester);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url, Collections.unmodifiableMap(map));
 
-        // verify that the request also includes X-Requested-With header
-        // but is not overwritten by the webview
-        HttpRequest request = mWebServer.getLastRequest(TestHtmlConstants.HELLO_WORLD_URL);
-        Header[] matchingHeaders = request.getHeaders(X_REQUESTED_WITH);
-        assertEquals(1, matchingHeaders.length);
+                    // verify that the request also includes X-Requested-With header
+                    // but is not overwritten by the webview
+                    HttpRequest request =
+                            mWebServer.getLastAssetRequest(TestHtmlConstants.HELLO_WORLD_URL);
+                    String[] matchingHeaders = request.getHeaders(X_REQUESTED_WITH);
+                    assertEquals(1, matchingHeaders.length);
 
-        Header header = matchingHeaders[0];
-        assertEquals(requester, header.getValue());
+                    String header = matchingHeaders[0];
+                    assertEquals(requester, header);
+                });
     }
 
+    @Test
     public void testCanInjectHeaders() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final String X_FOO = "X-foo";
         final String X_FOO_VALUE = "test";
 
         final String X_REFERER = "Referer";
         final String X_REFERER_VALUE = "http://www.example.com/";
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         HashMap<String, String> map = new HashMap<String, String>();
         map.put(X_FOO, X_FOO_VALUE);
         map.put(X_REFERER, X_REFERER_VALUE);
         mOnUiThread.loadUrlAndWaitForCompletion(url, map);
 
-        HttpRequest request = mWebServer.getLastRequest(TestHtmlConstants.HELLO_WORLD_URL);
-        for (Map.Entry<String,String> value : map.entrySet()) {
+        HttpRequest request = mWebServer.getLastAssetRequest(TestHtmlConstants.HELLO_WORLD_URL);
+        for (Map.Entry<String, String> value : map.entrySet()) {
             String header = value.getKey();
-            Header[] matchingHeaders = request.getHeaders(header);
+            String[] matchingHeaders = request.getHeaders(header);
             assertEquals("header " + header + " not found", 1, matchingHeaders.length);
-            assertEquals(value.getValue(), matchingHeaders[0].getValue());
+            assertEquals(value.getValue(), matchingHeaders[0]);
         }
     }
 
+    @Test
     @SuppressWarnings("deprecation")
-    @UiThreadTest
     public void testGetVisibleTitleHeight() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
-        startWebServer(false);
-        String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
-        mOnUiThread.loadUrlAndWaitForCompletion(url);
-        assertEquals(0, mWebView.getVisibleTitleHeight());
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url);
+                    assertEquals(0, mWebView.getVisibleTitleHeight());
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testGetOriginalUrl() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
-        startWebServer(false);
-        final String finalUrl = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
-        final String redirectUrl =
-                mWebServer.getRedirectingAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    final String finalUrl =
+                            mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
+                    final String redirectUrl =
+                            mWebServer.getRedirectingAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
 
-        assertNull(mWebView.getUrl());
-        assertNull(mWebView.getOriginalUrl());
+                    assertNull(mWebView.getUrl());
+                    assertNull(mWebView.getOriginalUrl());
 
-        // By default, WebView sends an intent to ask the system to
-        // handle loading a new URL. We set a WebViewClient as
-        // WebViewClient.shouldOverrideUrlLoading() returns false, so
-        // the WebView will load the new URL.
-        mWebView.setWebViewClient(new WaitForLoadedClient(mOnUiThread));
-        mOnUiThread.loadUrlAndWaitForCompletion(redirectUrl);
+                    // By default, WebView sends an intent to ask the system to
+                    // handle loading a new URL. We set a WebViewClient as
+                    // WebViewClient.shouldOverrideUrlLoading() returns false, so
+                    // the WebView will load the new URL.
+                    mWebView.setWebViewClient(new WaitForLoadedClient(mOnUiThread));
+                    mOnUiThread.loadUrlAndWaitForCompletion(redirectUrl);
 
-        assertEquals(finalUrl, mWebView.getUrl());
-        assertEquals(redirectUrl, mWebView.getOriginalUrl());
+                    assertEquals(finalUrl, mWebView.getUrl());
+                    assertEquals(redirectUrl, mWebView.getOriginalUrl());
+                });
     }
 
+    @Test
     public void testStopLoading() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         assertEquals(INITIAL_PROGRESS, mOnUiThread.getProgress());
 
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         String url = mWebServer.getDelayedAssetUrl(TestHtmlConstants.STOP_LOADING_URL);
 
         class JsInterface {
@@ -514,6 +508,7 @@
                 mPageLoaded = true;
                 notify();
             }
+
             public synchronized boolean getPageLoaded() {
                 return mPageLoaded;
             }
@@ -534,61 +529,58 @@
         assertFalse(jsInterface.getPageLoaded());
     }
 
-    @UiThreadTest
+    @Test
     public void testGoBackAndForward() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
-        assertGoBackOrForwardBySteps(false, -1);
-        assertGoBackOrForwardBySteps(false, 1);
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    assertGoBackOrForwardBySteps(false, -1);
+                    assertGoBackOrForwardBySteps(false, 1);
 
-        startWebServer(false);
-        String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
-        String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
-        String url3 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL3);
+                    String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
+                    String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
+                    String url3 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL3);
 
-        mOnUiThread.loadUrlAndWaitForCompletion(url1);
-        pollingCheckWebBackForwardList(url1, 0, 1);
-        assertGoBackOrForwardBySteps(false, -1);
-        assertGoBackOrForwardBySteps(false, 1);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url1);
+                    pollingCheckWebBackForwardList(url1, 0, 1);
+                    assertGoBackOrForwardBySteps(false, -1);
+                    assertGoBackOrForwardBySteps(false, 1);
 
-        mOnUiThread.loadUrlAndWaitForCompletion(url2);
-        pollingCheckWebBackForwardList(url2, 1, 2);
-        assertGoBackOrForwardBySteps(true, -1);
-        assertGoBackOrForwardBySteps(false, 1);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url2);
+                    pollingCheckWebBackForwardList(url2, 1, 2);
+                    assertGoBackOrForwardBySteps(true, -1);
+                    assertGoBackOrForwardBySteps(false, 1);
 
-        mOnUiThread.loadUrlAndWaitForCompletion(url3);
-        pollingCheckWebBackForwardList(url3, 2, 3);
-        assertGoBackOrForwardBySteps(true, -2);
-        assertGoBackOrForwardBySteps(false, 1);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url3);
+                    pollingCheckWebBackForwardList(url3, 2, 3);
+                    assertGoBackOrForwardBySteps(true, -2);
+                    assertGoBackOrForwardBySteps(false, 1);
 
-        mWebView.goBack();
-        pollingCheckWebBackForwardList(url2, 1, 3);
-        assertGoBackOrForwardBySteps(true, -1);
-        assertGoBackOrForwardBySteps(true, 1);
+                    mWebView.goBack();
+                    pollingCheckWebBackForwardList(url2, 1, 3);
+                    assertGoBackOrForwardBySteps(true, -1);
+                    assertGoBackOrForwardBySteps(true, 1);
 
-        mWebView.goForward();
-        pollingCheckWebBackForwardList(url3, 2, 3);
-        assertGoBackOrForwardBySteps(true, -2);
-        assertGoBackOrForwardBySteps(false, 1);
+                    mWebView.goForward();
+                    pollingCheckWebBackForwardList(url3, 2, 3);
+                    assertGoBackOrForwardBySteps(true, -2);
+                    assertGoBackOrForwardBySteps(false, 1);
 
-        mWebView.goBackOrForward(-2);
-        pollingCheckWebBackForwardList(url1, 0, 3);
-        assertGoBackOrForwardBySteps(false, -1);
-        assertGoBackOrForwardBySteps(true, 2);
+                    mWebView.goBackOrForward(-2);
+                    pollingCheckWebBackForwardList(url1, 0, 3);
+                    assertGoBackOrForwardBySteps(false, -1);
+                    assertGoBackOrForwardBySteps(true, 2);
 
-        mWebView.goBackOrForward(2);
-        pollingCheckWebBackForwardList(url3, 2, 3);
-        assertGoBackOrForwardBySteps(true, -2);
-        assertGoBackOrForwardBySteps(false, 1);
+                    mWebView.goBackOrForward(2);
+                    pollingCheckWebBackForwardList(url3, 2, 3);
+                    assertGoBackOrForwardBySteps(true, -2);
+                    assertGoBackOrForwardBySteps(false, 1);
+                });
     }
 
+    @Test
     public void testAddJavascriptInterface() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         mOnUiThread.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
 
@@ -626,84 +618,73 @@
         mOnUiThread.addJavascriptInterface(obj, "interface");
         assertFalse(obj.wasProvideResultCalled());
 
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         String url = mWebServer.getAssetUrl(TestHtmlConstants.ADD_JAVA_SCRIPT_INTERFACE_URL);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
         assertEquals("Original title", obj.waitForResult());
 
         // Verify that only methods annotated with @JavascriptInterface are exposed
         // on the JavaScript interface object.
-        assertEquals("\"function\"",
+        assertEquals(
+                "\"function\"",
                 mOnUiThread.evaluateJavascriptSync("typeof interface.provideResult"));
 
-        assertEquals("\"undefined\"",
+        assertEquals(
+                "\"undefined\"",
                 mOnUiThread.evaluateJavascriptSync("typeof interface.wasProvideResultCalled"));
 
-        assertEquals("\"undefined\"",
-                mOnUiThread.evaluateJavascriptSync("typeof interface.getClass"));
+        assertEquals(
+                "\"undefined\"", mOnUiThread.evaluateJavascriptSync("typeof interface.getClass"));
     }
 
+    @Test
     public void testAddJavascriptInterfaceNullObject() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
-        String setTitleToPropertyTypeHtml = "<html><head></head>" +
-                "<body onload=\"document.title = typeof window.injectedObject;\"></body></html>";
+        String setTitleToPropertyTypeHtml =
+                "<html><head></head><body onload=\"document.title = typeof"
+                        + " window.injectedObject;\"></body></html>";
 
         // Test that the property is initially undefined.
-        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml,
-                "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
         assertEquals("undefined", mOnUiThread.getTitle());
 
         // Test that adding a null object has no effect.
         mOnUiThread.addJavascriptInterface(null, "injectedObject");
-        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml,
-                "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
         assertEquals("undefined", mOnUiThread.getTitle());
 
         // Test that adding an object gives an object type.
         final Object obj = new Object();
         mOnUiThread.addJavascriptInterface(obj, "injectedObject");
-        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml,
-                "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
         assertEquals("object", mOnUiThread.getTitle());
 
         // Test that trying to replace with a null object has no effect.
         mOnUiThread.addJavascriptInterface(null, "injectedObject");
-        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml,
-                "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
         assertEquals("object", mOnUiThread.getTitle());
     }
 
+    @Test
     public void testRemoveJavascriptInterface() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
-        String setTitleToPropertyTypeHtml = "<html><head></head>" +
-                "<body onload=\"document.title = typeof window.injectedObject;\"></body></html>";
+        String setTitleToPropertyTypeHtml =
+                "<html><head></head><body onload=\"document.title = typeof"
+                        + " window.injectedObject;\"></body></html>";
 
         // Test that adding an object gives an object type.
         mOnUiThread.addJavascriptInterface(new Object(), "injectedObject");
-        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml,
-                "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
         assertEquals("object", mOnUiThread.getTitle());
 
         // Test that reloading the page after removing the object leaves the property undefined.
         mOnUiThread.removeJavascriptInterface("injectedObject");
-        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml,
-                "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
         assertEquals("undefined", mOnUiThread.getTitle());
     }
 
+    @Test
     public void testUseRemovedJavascriptInterface() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         class RemovedObject {
             @Override
             @JavascriptInterface
@@ -727,6 +708,7 @@
                 mIsResultAvailable = true;
                 notify();
             }
+
             public synchronized String getResult() {
                 while (!mIsResultAvailable) {
                     try {
@@ -744,30 +726,31 @@
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         mOnUiThread.addJavascriptInterface(new RemovedObject(), "removedObject");
         mOnUiThread.addJavascriptInterface(resultObject, "resultObject");
-        mOnUiThread.loadDataAndWaitForCompletion("<html><head></head>" +
-                "<body onload=\"window.removedObject.remove();" +
-                "resultObject.setResult(removedObject.toString());\"></body></html>",
-                "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><head></head>"
+                        + "<body onload=\"window.removedObject.remove();"
+                        + "resultObject.setResult(removedObject.toString());\"></body></html>",
+                "text/html",
+                null);
         assertEquals("removedObject", resultObject.getResult());
     }
 
+    @Test
     public void testAddJavascriptInterfaceExceptions() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         WebSettings settings = mOnUiThread.getSettings();
         settings.setJavaScriptEnabled(true);
         settings.setJavaScriptCanOpenWindowsAutomatically(true);
 
-        final AtomicBoolean mJsInterfaceWasCalled = new AtomicBoolean(false) {
-            @JavascriptInterface
-            public synchronized void call() {
-                set(true);
-                // The main purpose of this test is to ensure an exception here does not
-                // crash the implementation.
-                throw new RuntimeException("Javascript Interface exception");
-            }
-        };
+        final AtomicBoolean mJsInterfaceWasCalled =
+                new AtomicBoolean(false) {
+                    @JavascriptInterface
+                    public synchronized void call() {
+                        set(true);
+                        // The main purpose of this test is to ensure an exception here does not
+                        // crash the implementation.
+                        throw new RuntimeException("Javascript Interface exception");
+                    }
+                };
 
         mOnUiThread.addJavascriptInterface(mJsInterfaceWasCalled, "interface");
 
@@ -775,16 +758,15 @@
 
         assertFalse(mJsInterfaceWasCalled.get());
 
-        assertEquals("\"pass\"", mOnUiThread.evaluateJavascriptSync(
-                "try {interface.call(); 'fail'; } catch (exception) { 'pass'; } "));
+        assertEquals(
+                "\"pass\"",
+                mOnUiThread.evaluateJavascriptSync(
+                        "try {interface.call(); 'fail'; } catch (exception) { 'pass'; } "));
         assertTrue(mJsInterfaceWasCalled.get());
     }
 
+    @Test
     public void testJavascriptInterfaceCustomPropertiesClearedOnReload() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
 
         mOnUiThread.addJavascriptInterface(new Object(), "interface");
@@ -799,12 +781,9 @@
         assertEquals("false", mOnUiThread.evaluateJavascriptSync("'custom_property' in interface"));
     }
 
+    @Test
     @FlakyTest(bugId = 171702662)
     public void testJavascriptInterfaceForClientPopup() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         mOnUiThread.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
         mOnUiThread.getSettings().setSupportMultipleWindows(true);
@@ -823,31 +802,40 @@
         childOnUiThread.addJavascriptInterface(obj, "interface");
 
         final SettableFuture<Void> onCreateWindowFuture = SettableFuture.create();
-        mOnUiThread.setWebChromeClient(new WebViewSyncLoader.WaitForProgressClient(mOnUiThread) {
-            @Override
-            public boolean onCreateWindow(
-                WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
-                getActivity().addContentView(childWebView, new ViewGroup.LayoutParams(
-                            ViewGroup.LayoutParams.FILL_PARENT,
-                            ViewGroup.LayoutParams.WRAP_CONTENT));
-                WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
-                transport.setWebView(childWebView);
-                resultMsg.sendToTarget();
-                onCreateWindowFuture.set(null);
-                return true;
-            }
-        });
+        mOnUiThread.setWebChromeClient(
+                new WebViewSyncLoader.WaitForProgressClient(mOnUiThread) {
+                    @Override
+                    public boolean onCreateWindow(
+                            WebView view,
+                            boolean isDialog,
+                            boolean isUserGesture,
+                            Message resultMsg) {
+                        getTestEnvironment().addContentView(
+                                childWebView,
+                                new ViewGroup.LayoutParams(
+                                        ViewGroup.LayoutParams.FILL_PARENT,
+                                        ViewGroup.LayoutParams.WRAP_CONTENT));
+                        WebView.WebViewTransport transport =
+                                (WebView.WebViewTransport) resultMsg.obj;
+                        transport.setWebView(childWebView);
+                        resultMsg.sendToTarget();
+                        onCreateWindowFuture.set(null);
+                        return true;
+                    }
+                });
 
-        startWebServer(false);
-        mOnUiThread.loadUrlAndWaitForCompletion(mWebServer.
-                getAssetUrl(TestHtmlConstants.POPUP_URL));
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
+        mOnUiThread.loadUrlAndWaitForCompletion(
+                mWebServer.getAssetUrl(TestHtmlConstants.POPUP_URL));
         WebkitUtils.waitForFuture(onCreateWindowFuture);
 
         childOnUiThread.loadUrlAndWaitForCompletion("about:blank");
 
         assertEquals("true", childOnUiThread.evaluateJavascriptSync("'interface' in window"));
 
-        assertEquals("The injected object should be functional", "42",
+        assertEquals(
+                "The injected object should be functional",
+                "42",
                 childOnUiThread.evaluateJavascriptSync("interface.test()"));
     }
 
@@ -863,17 +851,17 @@
         }
     }
 
-    private Picture waitForPictureToHaveColor(int color,
-            final TestPictureListener listener) throws Throwable {
+    private Picture waitForPictureToHaveColor(int color, final TestPictureListener listener)
+            throws Throwable {
         final int MAX_ON_NEW_PICTURE_ITERATIONS = 5;
         final AtomicReference<Picture> pictureRef = new AtomicReference<Picture>();
         for (int i = 0; i < MAX_ON_NEW_PICTURE_ITERATIONS; i++) {
             final int oldCallCount = listener.callCount;
-            WebkitUtils.onMainThreadSync(() -> {
-                pictureRef.set(mWebView.capturePicture());
-            });
-            if (isPictureFilledWithColor(pictureRef.get(), color))
-                break;
+            WebkitUtils.onMainThreadSync(
+                    () -> {
+                        pictureRef.set(mWebView.capturePicture());
+                    });
+            if (isPictureFilledWithColor(pictureRef.get(), color)) break;
             new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
                 @Override
                 protected boolean check() {
@@ -884,13 +872,11 @@
         return pictureRef.get();
     }
 
+    @Test
     public void testCapturePicture() throws Exception, Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final TestPictureListener listener = new TestPictureListener();
 
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.BLANK_PAGE_URL);
         mOnUiThread.setPictureListener(listener);
         // Showing the blank page will fill the picture with the background color.
@@ -898,20 +884,20 @@
         // The default background color is white.
         Picture oldPicture = waitForPictureToHaveColor(Color.WHITE, listener);
 
-        WebkitUtils.onMainThread(() -> {
-            mWebView.setBackgroundColor(Color.CYAN);
-        });
+        WebkitUtils.onMainThread(
+                () -> {
+                    mWebView.setBackgroundColor(Color.CYAN);
+                });
         mOnUiThread.reloadAndWaitForCompletion();
         waitForPictureToHaveColor(Color.CYAN, listener);
 
-        assertTrue("The content of the previously captured picture should not update automatically",
+        assertTrue(
+                "The content of the previously captured picture should not update automatically",
                 isPictureFilledWithColor(oldPicture, Color.WHITE));
     }
 
+    @Test
     public void testSetPictureListener() throws Exception, Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final class MyPictureListener implements PictureListener {
             public int callCount;
             public WebView webView;
@@ -929,7 +915,7 @@
         }
 
         final MyPictureListener listener = new MyPictureListener();
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         mOnUiThread.setPictureListener(listener);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
@@ -953,170 +939,171 @@
         }.run();
     }
 
-    @UiThreadTest
+    @Test
     public void testAccessHttpAuthUsernamePassword() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        try {
-            WebViewDatabase.getInstance(getActivity()).clearHttpAuthUsernamePassword();
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    try {
+                        WebViewDatabase.getInstance(mContext).clearHttpAuthUsernamePassword();
 
-            String host = "http://localhost:8080";
-            String realm = "testrealm";
-            String userName = "user";
-            String password = "password";
+                        String host = "http://localhost:8080";
+                        String realm = "testrealm";
+                        String userName = "user";
+                        String password = "password";
 
-            String[] result = mWebView.getHttpAuthUsernamePassword(host, realm);
-            assertNull(result);
+                        String[] result = mWebView.getHttpAuthUsernamePassword(host, realm);
+                        assertNull(result);
 
-            mWebView.setHttpAuthUsernamePassword(host, realm, userName, password);
-            result = mWebView.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(userName, result[0]);
-            assertEquals(password, result[1]);
+                        mWebView.setHttpAuthUsernamePassword(host, realm, userName, password);
+                        result = mWebView.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(userName, result[0]);
+                        assertEquals(password, result[1]);
 
-            String newPassword = "newpassword";
-            mWebView.setHttpAuthUsernamePassword(host, realm, userName, newPassword);
-            result = mWebView.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(userName, result[0]);
-            assertEquals(newPassword, result[1]);
+                        String newPassword = "newpassword";
+                        mWebView.setHttpAuthUsernamePassword(host, realm, userName, newPassword);
+                        result = mWebView.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(userName, result[0]);
+                        assertEquals(newPassword, result[1]);
 
-            String newUserName = "newuser";
-            mWebView.setHttpAuthUsernamePassword(host, realm, newUserName, newPassword);
-            result = mWebView.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(newUserName, result[0]);
-            assertEquals(newPassword, result[1]);
+                        String newUserName = "newuser";
+                        mWebView.setHttpAuthUsernamePassword(host, realm, newUserName, newPassword);
+                        result = mWebView.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(newUserName, result[0]);
+                        assertEquals(newPassword, result[1]);
 
-            // the user is set to null, can not change any thing in the future
-            mWebView.setHttpAuthUsernamePassword(host, realm, null, password);
-            result = mWebView.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertNull(result[0]);
-            assertEquals(password, result[1]);
+                        // the user is set to null, can not change any thing in the future
+                        mWebView.setHttpAuthUsernamePassword(host, realm, null, password);
+                        result = mWebView.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertNull(result[0]);
+                        assertEquals(password, result[1]);
 
-            mWebView.setHttpAuthUsernamePassword(host, realm, userName, null);
-            result = mWebView.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(userName, result[0]);
-            assertNull(result[1]);
+                        mWebView.setHttpAuthUsernamePassword(host, realm, userName, null);
+                        result = mWebView.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(userName, result[0]);
+                        assertNull(result[1]);
 
-            mWebView.setHttpAuthUsernamePassword(host, realm, null, null);
-            result = mWebView.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertNull(result[0]);
-            assertNull(result[1]);
+                        mWebView.setHttpAuthUsernamePassword(host, realm, null, null);
+                        result = mWebView.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertNull(result[0]);
+                        assertNull(result[1]);
 
-            mWebView.setHttpAuthUsernamePassword(host, realm, newUserName, newPassword);
-            result = mWebView.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(newUserName, result[0]);
-            assertEquals(newPassword, result[1]);
-        } finally {
-            WebViewDatabase.getInstance(getActivity()).clearHttpAuthUsernamePassword();
-        }
+                        mWebView.setHttpAuthUsernamePassword(host, realm, newUserName, newPassword);
+                        result = mWebView.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(newUserName, result[0]);
+                        assertEquals(newPassword, result[1]);
+                    } finally {
+                        WebViewDatabase.getInstance(mContext).clearHttpAuthUsernamePassword();
+                    }
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testWebViewDatabaseAccessHttpAuthUsernamePassword() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        WebViewDatabase webViewDb = WebViewDatabase.getInstance(getActivity());
-        try {
-            webViewDb.clearHttpAuthUsernamePassword();
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    WebViewDatabase webViewDb = WebViewDatabase.getInstance(mContext);
+                    try {
+                        webViewDb.clearHttpAuthUsernamePassword();
 
-            String host = "http://localhost:8080";
-            String realm = "testrealm";
-            String userName = "user";
-            String password = "password";
+                        String host = "http://localhost:8080";
+                        String realm = "testrealm";
+                        String userName = "user";
+                        String password = "password";
 
-            String[] result =
-                    mWebView.getHttpAuthUsernamePassword(host,
-                            realm);
-            assertNull(result);
+                        String[] result = mWebView.getHttpAuthUsernamePassword(host, realm);
+                        assertNull(result);
 
-            webViewDb.setHttpAuthUsernamePassword(host, realm, userName, password);
-            result = webViewDb.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(userName, result[0]);
-            assertEquals(password, result[1]);
+                        webViewDb.setHttpAuthUsernamePassword(host, realm, userName, password);
+                        result = webViewDb.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(userName, result[0]);
+                        assertEquals(password, result[1]);
 
-            String newPassword = "newpassword";
-            webViewDb.setHttpAuthUsernamePassword(host, realm, userName, newPassword);
-            result = webViewDb.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(userName, result[0]);
-            assertEquals(newPassword, result[1]);
+                        String newPassword = "newpassword";
+                        webViewDb.setHttpAuthUsernamePassword(host, realm, userName, newPassword);
+                        result = webViewDb.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(userName, result[0]);
+                        assertEquals(newPassword, result[1]);
 
-            String newUserName = "newuser";
-            webViewDb.setHttpAuthUsernamePassword(host, realm, newUserName, newPassword);
-            result = webViewDb.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(newUserName, result[0]);
-            assertEquals(newPassword, result[1]);
+                        String newUserName = "newuser";
+                        webViewDb.setHttpAuthUsernamePassword(
+                                host, realm, newUserName, newPassword);
+                        result = webViewDb.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(newUserName, result[0]);
+                        assertEquals(newPassword, result[1]);
 
-            // the user is set to null, can not change any thing in the future
-            webViewDb.setHttpAuthUsernamePassword(host, realm, null, password);
-            result = webViewDb.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertNull(result[0]);
-            assertEquals(password, result[1]);
+                        // the user is set to null, can not change any thing in the future
+                        webViewDb.setHttpAuthUsernamePassword(host, realm, null, password);
+                        result = webViewDb.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertNull(result[0]);
+                        assertEquals(password, result[1]);
 
-            webViewDb.setHttpAuthUsernamePassword(host, realm, userName, null);
-            result = webViewDb.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(userName, result[0]);
-            assertNull(result[1]);
+                        webViewDb.setHttpAuthUsernamePassword(host, realm, userName, null);
+                        result = webViewDb.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(userName, result[0]);
+                        assertNull(result[1]);
 
-            webViewDb.setHttpAuthUsernamePassword(host, realm, null, null);
-            result = webViewDb.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertNull(result[0]);
-            assertNull(result[1]);
+                        webViewDb.setHttpAuthUsernamePassword(host, realm, null, null);
+                        result = webViewDb.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertNull(result[0]);
+                        assertNull(result[1]);
 
-            webViewDb.setHttpAuthUsernamePassword(host, realm, newUserName, newPassword);
-            result = webViewDb.getHttpAuthUsernamePassword(host, realm);
-            assertNotNull(result);
-            assertEquals(newUserName, result[0]);
-            assertEquals(newPassword, result[1]);
-        } finally {
-            webViewDb.clearHttpAuthUsernamePassword();
-        }
+                        webViewDb.setHttpAuthUsernamePassword(
+                                host, realm, newUserName, newPassword);
+                        result = webViewDb.getHttpAuthUsernamePassword(host, realm);
+                        assertNotNull(result);
+                        assertEquals(newUserName, result[0]);
+                        assertEquals(newPassword, result[1]);
+                    } finally {
+                        webViewDb.clearHttpAuthUsernamePassword();
+                    }
+                });
     }
 
+    @Test
     public void testLoadData() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final String firstTitle = "Hello, World!";
         final String HTML_CONTENT =
-                "<html><head><title>" + firstTitle + "</title></head><body></body>" +
-                "</html>";
-        mOnUiThread.loadDataAndWaitForCompletion(HTML_CONTENT,
-                "text/html", null);
+                "<html><head><title>" + firstTitle + "</title></head><body></body>" + "</html>";
+        mOnUiThread.loadDataAndWaitForCompletion(HTML_CONTENT, "text/html", null);
         assertEquals(firstTitle, mOnUiThread.getTitle());
 
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String crossOriginUrl = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         final String secondTitle = "Foo bar";
         mOnUiThread.loadDataAndWaitForCompletion(
-                "<html><head><title>" + secondTitle + "</title></head><body onload=\"" +
-                "document.title = " +
-                "document.getElementById('frame').contentWindow.location.href;" +
-                "\"><iframe id=\"frame\" src=\"" + crossOriginUrl + "\"></body></html>",
-                "text/html", null);
-        assertEquals("Page title should not change, because it should be an error to access a "
-                + "cross-site frame's href.",
-                secondTitle, mOnUiThread.getTitle());
+                "<html><head><title>"
+                        + secondTitle
+                        + "</title></head><body onload=\""
+                        + "document.title = "
+                        + "document.getElementById('frame').contentWindow.location.href;"
+                        + "\"><iframe id=\"frame\" src=\""
+                        + crossOriginUrl
+                        + "\"></body></html>",
+                "text/html",
+                null);
+        assertEquals(
+                "Page title should not change, because it should be an error to access a "
+                        + "cross-site frame's href.",
+                secondTitle,
+                mOnUiThread.getTitle());
     }
 
+    @Test
     public void testLoadDataWithBaseUrl_resolvesRelativeToBaseUrl() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertNull(mOnUiThread.getUrl());
         String imgUrl = TestHtmlConstants.SMALL_IMG_URL; // relative
 
@@ -1124,106 +1111,109 @@
         // will fail and we won't make a request to the test web server.
         // By using the test web server as the base URL we expect to see a request
         // for the relative URL in the test server.
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String baseUrl = mWebServer.getAssetUrl("foo.html");
         mWebServer.resetRequestState();
-        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(baseUrl,
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
+                baseUrl,
                 HTML_HEADER + "<body><img src=\"" + imgUrl + "\"/></body></html>",
-                "text/html", "UTF-8", null);
-        assertTrue("The resource request should make it to the server",
+                "text/html",
+                "UTF-8",
+                null);
+        assertTrue(
+                "The resource request should make it to the server",
                 mWebServer.wasResourceRequested(imgUrl));
     }
 
+    @Test
     public void testLoadDataWithBaseUrl_historyUrl() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final String baseUrl = "http://www.baseurl.com/";
         final String historyUrl = "http://www.example.com/";
-        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(baseUrl,
-                SIMPLE_HTML,
-                "text/html", "UTF-8", historyUrl);
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
+                baseUrl, SIMPLE_HTML, "text/html", "UTF-8", historyUrl);
         assertEquals(historyUrl, mOnUiThread.getUrl());
     }
 
+    @Test
     public void testLoadDataWithBaseUrl_nullHistoryUrlShowsAsAboutBlank() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Check that reported URL is "about:blank" when supplied history URL
         // is null.
         final String baseUrl = "http://www.baseurl.com/";
-        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(baseUrl,
-                SIMPLE_HTML,
-                "text/html", "UTF-8", null);
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
+                baseUrl, SIMPLE_HTML, "text/html", "UTF-8", null);
         assertEquals("about:blank", mOnUiThread.getUrl());
     }
 
+    @Test
     public void testLoadDataWithBaseUrl_javascriptCanAccessOrigin() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Test that JavaScript can access content from the same origin as the base URL.
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String baseUrl = mWebServer.getAssetUrl("foo.html");
         final String crossOriginUrl = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
-        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(baseUrl,
-                HTML_HEADER + "<body onload=\"" +
-                "document.title = document.getElementById('frame').contentWindow.location.href;" +
-                "\"><iframe id=\"frame\" src=\"" + crossOriginUrl + "\"></body></html>",
-                "text/html", "UTF-8", null);
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
+                baseUrl,
+                HTML_HEADER
+                        + "<body onload=\"document.title ="
+                        + " document.getElementById('frame').contentWindow.location.href;\"><iframe"
+                        + " id=\"frame\" src=\""
+                        + crossOriginUrl
+                        + "\"></body></html>",
+                "text/html",
+                "UTF-8",
+                null);
         assertEquals(crossOriginUrl, mOnUiThread.getTitle());
     }
 
+    @Test
     public void testLoadDataWithBaseUrl_dataBaseUrlIgnoresHistoryUrl() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Check that when the base URL uses the 'data' scheme, a 'data' scheme URL is used and the
         // history URL is ignored.
         final String baseUrl = "data:foo";
         final String historyUrl = "http://www.example.com/";
-        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(baseUrl,
-                SIMPLE_HTML,
-                "text/html", "UTF-8", historyUrl);
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
+                baseUrl, SIMPLE_HTML, "text/html", "UTF-8", historyUrl);
 
         final String currentUrl = mOnUiThread.getUrl();
-        assertEquals("Current URL (" + currentUrl + ") should be a data URI", 0,
+        assertEquals(
+                "Current URL (" + currentUrl + ") should be a data URI",
+                0,
                 mOnUiThread.getUrl().indexOf("data:text/html"));
-        assertThat("Current URL (" + currentUrl + ") should contain the simple HTML we loaded",
-                mOnUiThread.getUrl().indexOf("simple html"), greaterThan(0));
+        assertThat(
+                "Current URL (" + currentUrl + ") should contain the simple HTML we loaded",
+                mOnUiThread.getUrl().indexOf("simple html"),
+                greaterThan(0));
     }
 
+    @Test
     public void testLoadDataWithBaseUrl_unencodedContentHttpBaseUrl() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Check that when a non-data: base URL is used, we treat the String to load as
         // a raw string and just dump it into the WebView, i.e. not decoding any URL entities.
-        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion("http://www.foo.com",
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
+                "http://www.foo.com",
                 HTML_HEADER + "<title>Hello World%21</title><body>bar</body></html>",
-                "text/html", "UTF-8", null);
+                "text/html",
+                "UTF-8",
+                null);
         assertEquals("Hello World%21", mOnUiThread.getTitle());
     }
 
+    @Test
     public void testLoadDataWithBaseUrl_urlEncodedContentDataBaseUrl() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Check that when a data: base URL is used, we treat the String to load as a data: URL
         // and run load steps such as decoding URL entities (i.e., contrary to the test case
         // above.)
-        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion("data:foo",
-                HTML_HEADER + "<title>Hello World%21</title></html>", "text/html", "UTF-8", null);
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
+                "data:foo",
+                HTML_HEADER + "<title>Hello World%21</title></html>",
+                "text/html",
+                "UTF-8",
+                null);
         assertEquals("Hello World!", mOnUiThread.getTitle());
     }
 
+    @Test
     public void testLoadDataWithBaseUrl_nullSafe() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(null, null, null, null, null);
         assertEquals("about:blank", mOnUiThread.getUrl());
     }
@@ -1237,7 +1227,7 @@
     private String readTextFile(File file, Charset encoding)
             throws FileNotFoundException, IOException {
         FileInputStream stream = new FileInputStream(file);
-        byte[] bytes = new byte[(int)file.length()];
+        byte[] bytes = new byte[(int) file.length()];
         stream.read(bytes);
         stream.close();
         return new String(bytes, encoding);
@@ -1246,26 +1236,24 @@
     private void doSaveWebArchive(String baseName, boolean autoName, final String expectName)
             throws Throwable {
         final Semaphore saving = new Semaphore(0);
-        ValueCallback<String> callback = new ValueCallback<String>() {
-            @Override
-            public void onReceiveValue(String savedName) {
-                assertEquals(expectName, savedName);
-                saving.release();
-            }
-        };
+        ValueCallback<String> callback =
+                new ValueCallback<String>() {
+                    @Override
+                    public void onReceiveValue(String savedName) {
+                        assertEquals(expectName, savedName);
+                        saving.release();
+                    }
+                };
 
         mOnUiThread.saveWebArchive(baseName, autoName, callback);
         assertTrue(saving.tryAcquire(WebkitUtils.TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
     }
 
+    @Test
     public void testSaveWebArchive() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final String testPage = "testSaveWebArchive test page";
 
-        File dir = getActivity().getFilesDir();
+        File dir = mContext.getFilesDir();
         String dirStr = dir.toString();
 
         File test = new File(dir, "test.mht");
@@ -1316,16 +1304,14 @@
         }
     }
 
-    private static class WaitForFindResultsListener
-            implements WebView.FindListener {
+    private static class WaitForFindResultsListener implements WebView.FindListener {
         private final SettableFuture<Integer> mFuture;
         private final WebView mWebView;
         private final int mMatchesWanted;
         private final String mStringWanted;
         private final boolean mRetry;
 
-        public WaitForFindResultsListener(
-                WebView wv, String wanted, int matches, boolean retry) {
+        WaitForFindResultsListener(WebView wv, String wanted, int matches, boolean retry) {
             mFuture = SettableFuture.create();
             mWebView = wv;
             mMatchesWanted = matches;
@@ -1338,62 +1324,66 @@
         }
 
         @Override
-        public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches,
-                boolean isDoneCounting) {
+        public void onFindResultReceived(
+                int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
             try {
-                assertEquals("WebView.FindListener callbacks should occur on the UI thread",
-                        Looper.myLooper(), Looper.getMainLooper());
+                assertEquals(
+                        "WebView.FindListener callbacks should occur on the UI thread",
+                        Looper.myLooper(),
+                        Looper.getMainLooper());
             } catch (Throwable t) {
                 mFuture.setException(t);
             }
             if (isDoneCounting) {
-                //If mRetry set to true and matches aren't equal, call findAll again
+                // If mRetry set to true and matches aren't equal, call findAll again
                 if (mRetry && numberOfMatches != mMatchesWanted) {
                     mWebView.findAll(mStringWanted);
-                }
-                else {
+                } else {
                     mFuture.set(numberOfMatches);
                 }
             }
         }
     }
 
-    public void testFindAll()  throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+    @Test
+    public void testFindAll() throws Throwable {
         // Make the page scrollable, so we can detect the scrolling to make sure the
         // content fully loaded.
         mOnUiThread.setInitialScale(100);
         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
         int dimension = Math.max(metrics.widthPixels, metrics.heightPixels);
         // create a paragraph high enough to take up the entire screen
-        String p = "<p style=\"height:" + dimension + "px;\">" +
-                "Find all instances of find on the page and highlight them.</p>";
+        String p =
+                "<p style=\"height:"
+                        + dimension
+                        + "px;\">"
+                        + "Find all instances of find on the page and highlight them.</p>";
 
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + "</body></html>", "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + "</body></html>", "text/html", null);
 
         WaitForFindResultsListener l = new WaitForFindResultsListener(mWebView, "find", 2, true);
         mOnUiThread.setFindListener(l);
         mOnUiThread.findAll("find");
-        assertEquals(2, (int)WebkitUtils.waitForFuture(l.future()));
+        assertEquals(2, (int) WebkitUtils.waitForFuture(l.future()));
     }
 
+    @Test
     public void testFindNext() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         // Reset the scaling so that finding the next "all" text will require scrolling.
         mOnUiThread.setInitialScale(100);
 
         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
         int dimension = Math.max(metrics.widthPixels, metrics.heightPixels);
         // create a paragraph high enough to take up the entire screen
-        String p = "<p style=\"height:" + dimension + "px;\">" +
-                "Find all instances of a word on the page and highlight them.</p>";
+        String p =
+                "<p style=\"height:"
+                        + dimension
+                        + "px;\">"
+                        + "Find all instances of a word on the page and highlight them.</p>";
 
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p + p + "</body></html>", "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + p + "</body></html>", "text/html", null);
         WaitForFindResultsListener l = new WaitForFindResultsListener(mWebView, "all", 2, true);
         mOnUiThread.setFindListener(l);
 
@@ -1430,7 +1420,7 @@
 
         // clear the result
         mOnUiThread.clearMatches();
-        getInstrumentation().waitForIdleSync();
+        getTestEnvironment().waitForIdleSync();
 
         // can not scroll any more
         mOnUiThread.findNext(false);
@@ -1442,41 +1432,45 @@
         assertEquals(mOnUiThread.getScrollY(), previousScrollY);
     }
 
+    @Test
     public void testDocumentHasImages() throws Exception, Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final class DocumentHasImageCheckHandler extends Handler {
             private SettableFuture<Integer> mFuture;
+
             public DocumentHasImageCheckHandler(Looper looper) {
                 super(looper);
                 mFuture = SettableFuture.create();
             }
+
             @Override
             public void handleMessage(Message msg) {
                 mFuture.set(msg.arg1);
             }
+
             public Future<Integer> future() {
                 return mFuture;
             }
         }
 
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String imgUrl = mWebServer.getAssetUrl(TestHtmlConstants.SMALL_IMG_URL);
 
         // Create a handler on the UI thread.
         final DocumentHasImageCheckHandler handler =
-            new DocumentHasImageCheckHandler(mWebView.getHandler().getLooper());
+                new DocumentHasImageCheckHandler(mWebView.getHandler().getLooper());
 
-        WebkitUtils.onMainThreadSync(() -> {
-            mOnUiThread.loadDataAndWaitForCompletion("<html><body><img src=\""
-                    + imgUrl + "\"/></body></html>", "text/html", null);
-            Message response = new Message();
-            response.setTarget(handler);
-            assertFalse(handler.future().isDone());
-            mWebView.documentHasImages(response);
-        });
-        assertEquals(1, (int)WebkitUtils.waitForFuture(handler.future()));
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    mOnUiThread.loadDataAndWaitForCompletion(
+                            "<html><body><img src=\"" + imgUrl + "\"/></body></html>",
+                            "text/html",
+                            null);
+                    Message response = new Message();
+                    response.setTarget(handler);
+                    assertFalse(handler.future().isDone());
+                    mWebView.documentHasImages(response);
+                });
+        assertEquals(1, (int) WebkitUtils.waitForFuture(handler.future()));
     }
 
     private static void waitForFlingDone(WebViewOnUiThread webview) {
@@ -1510,23 +1504,24 @@
         new ScrollDiffPollingCheck(webview).run();
     }
 
+    @Test
     public void testPageScroll() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
         int dimension = 2 * Math.max(metrics.widthPixels, metrics.heightPixels);
-        String p = "<p style=\"height:" + dimension + "px;\">" +
-                "Scroll by half the size of the page.</p>";
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + p + "</body></html>", "text/html", null);
+        String p =
+                "<p style=\"height:"
+                        + dimension
+                        + "px;\">"
+                        + "Scroll by half the size of the page.</p>";
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + p + "</body></html>", "text/html", null);
 
         // Wait for UI thread to settle and receive page dimentions from renderer
         // such that we can invoke page down.
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
-                 return mOnUiThread.pageDown(false);
+                return mOnUiThread.pageDown(false);
             }
         }.run();
 
@@ -1557,7 +1552,7 @@
 
         // jump to the top
         assertTrue(mOnUiThread.pageUp(true));
-         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
+        new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
                 return topScrollY == mOnUiThread.getScrollY();
@@ -1565,17 +1560,15 @@
         }.run();
     }
 
+    @Test
     public void testGetContentHeight() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mOnUiThread.loadDataAndWaitForCompletion(
-                "<html><body></body></html>", "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion("<html><body></body></html>", "text/html", null);
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
-                return mOnUiThread.getScale() != 0 && mOnUiThread.getContentHeight() != 0
-                    && mOnUiThread.getHeight() != 0;
+                return mOnUiThread.getScale() != 0
+                        && mOnUiThread.getContentHeight() != 0
+                        && mOnUiThread.getHeight() != 0;
             }
         }.run();
 
@@ -1603,14 +1596,17 @@
         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
         final float scaleFactor = Math.max(1.0f, 1.0f / mOnUiThread.getScale());
         final int pageHeight =
-                (int)(Math.ceil(Math.max(metrics.widthPixels, metrics.heightPixels)
-                * scaleFactor));
+                (int)
+                        (Math.ceil(
+                                Math.max(metrics.widthPixels, metrics.heightPixels) * scaleFactor));
 
         // set the margin to 0
-        final String p = "<p style=\"height:" + pageHeight
-                + "px;margin:0px auto;\">Get the height of HTML content.</p>";
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + "</body></html>", "text/html", null);
+        final String p =
+                "<p style=\"height:"
+                        + pageHeight
+                        + "px;margin:0px auto;\">Get the height of HTML content.</p>";
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + "</body></html>", "text/html", null);
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
@@ -1619,8 +1615,8 @@
         }.run();
 
         final int extraSpace = mOnUiThread.getContentHeight() - pageHeight;
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + p + "</body></html>", "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + p + "</body></html>", "text/html", null);
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
@@ -1635,53 +1631,58 @@
         }.run();
     }
 
-    @UiThreadTest
+    @Test
     public void testPlatformNotifications() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        WebView.enablePlatformNotifications();
-        WebView.disablePlatformNotifications();
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    WebView.enablePlatformNotifications();
+                    WebView.disablePlatformNotifications();
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testAccessPluginList() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        assertNotNull(WebView.getPluginList());
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    assertNotNull(WebView.getPluginList());
 
-        // can not find a way to install plugins
-        mWebView.refreshPlugins(false);
+                    // can not find a way to install plugins
+                    mWebView.refreshPlugins(false);
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testDestroy() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        // Create a new WebView, since we cannot call destroy() on a view in the hierarchy
-        WebView localWebView = new WebView(getActivity());
-        localWebView.destroy();
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    // Create a new WebView, since we cannot call destroy() on a view in the
+                    // hierarchy
+                    WebView localWebView = new WebView(mContext);
+                    localWebView.destroy();
+                });
     }
 
+    @Test
     public void testFlingScroll() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
         final int dimension = 10 * Math.max(metrics.widthPixels, metrics.heightPixels);
-        String p = "<p style=\"height:" + dimension + "px;" +
-                "width:" + dimension + "px\">Test fling scroll.</p>";
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + "</body></html>", "text/html", null);
+        String p =
+                "<p style=\"height:"
+                        + dimension
+                        + "px;"
+                        + "width:"
+                        + dimension
+                        + "px\">Test fling scroll.</p>";
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + "</body></html>", "text/html", null);
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
                 return mOnUiThread.getContentHeight() >= dimension;
             }
         }.run();
-        getInstrumentation().waitForIdleSync();
+
+        getTestEnvironment().waitForIdleSync();
 
         final int previousScrollX = mOnUiThread.getScrollX();
         final int previousScrollY = mOnUiThread.getScrollY();
@@ -1691,24 +1692,27 @@
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
-                return mOnUiThread.getScrollX() > previousScrollX &&
-                        mOnUiThread.getScrollY() > previousScrollY;
+                return mOnUiThread.getScrollX() > previousScrollX
+                        && mOnUiThread.getScrollY() > previousScrollY;
             }
         }.run();
     }
 
+    @Test
     public void testRequestFocusNodeHref() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        startWebServer(false);
-        String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
-        String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
-        final String links = "<DL><p><DT><A HREF=\"" + url1
-                + "\">HTML_URL1</A><DT><A HREF=\"" + url2
-                + "\">HTML_URL2</A></DL><p>";
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + links + "</body></html>", "text/html", null);
-        getInstrumentation().waitForIdleSync();
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
+
+        final String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
+        final String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
+        final String links =
+                "<DL><p><DT><A HREF=\""
+                        + url1
+                        + "\">HTML_URL1</A><DT><A HREF=\""
+                        + url2
+                        + "\">HTML_URL2</A></DL><p>";
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + links + "</body></html>", "text/html", null);
+        getTestEnvironment().waitForIdleSync();
 
         final HrefCheckHandler handler = new HrefCheckHandler(mWebView.getHandler().getLooper());
         final Message hrefMsg = new Message();
@@ -1716,7 +1720,7 @@
 
         // focus on first link
         handler.reset();
-        getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
+        getTestEnvironment().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
         mOnUiThread.requestFocusNodeHref(hrefMsg);
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
@@ -1741,16 +1745,14 @@
         handler.reset();
         final Message hrefMsg2 = new Message();
         hrefMsg2.setTarget(handler);
-        getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
+        getTestEnvironment().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
         mOnUiThread.requestFocusNodeHref(hrefMsg2);
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
                 boolean done = false;
-                final String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
                 if (handler.hasCalledHandleMessage()) {
-                    if (handler.mResultUrl != null &&
-                            handler.mResultUrl.equals(url2)) {
+                    if (handler.mResultUrl != null && handler.mResultUrl.equals(url2)) {
                         done = true;
                     } else {
                         handler.reset();
@@ -1767,10 +1769,8 @@
         mOnUiThread.requestFocusNodeHref(null);
     }
 
+    @Test
     public void testRequestImageRef() throws Exception, Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final class ImageLoaded {
             public SettableFuture<Void> mImageLoaded = SettableFuture.create();
 
@@ -1787,23 +1787,26 @@
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         mOnUiThread.addJavascriptInterface(imageLoaded, "imageLoaded");
 
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String imgUrl = mWebServer.getAssetUrl(TestHtmlConstants.LARGE_IMG_URL);
         mOnUiThread.loadDataAndWaitForCompletion(
                 "<html><head><title>Title</title><style type='text/css'>"
-                + "%23imgElement { -webkit-transform: translate3d(0,0,1); }"
-                + "%23imgElement.finish { -webkit-transform: translate3d(0,0,0);"
-                + " -webkit-transition-duration: 1ms; }</style>"
-                + "<script type='text/javascript'>function imgLoad() {"
-                + "imgElement = document.getElementById('imgElement');"
-                + "imgElement.addEventListener('webkitTransitionEnd',"
-                + "function(e) { imageLoaded.loaded(); });"
-                + "imgElement.className = 'finish';}</script>"
-                + "</head><body><img id='imgElement' src='" + imgUrl
-                + "' width='100%' height='100%' onLoad='imgLoad()'/>"
-                + "</body></html>", "text/html", null);
+                        + "%23imgElement { -webkit-transform: translate3d(0,0,1); }"
+                        + "%23imgElement.finish { -webkit-transform: translate3d(0,0,0);"
+                        + " -webkit-transition-duration: 1ms; }</style>"
+                        + "<script type='text/javascript'>function imgLoad() {"
+                        + "imgElement = document.getElementById('imgElement');"
+                        + "imgElement.addEventListener('webkitTransitionEnd',"
+                        + "function(e) { imageLoaded.loaded(); });"
+                        + "imgElement.className = 'finish';}</script>"
+                        + "</head><body><img id='imgElement' src='"
+                        + imgUrl
+                        + "' width='100%' height='100%' onLoad='imgLoad()'/>"
+                        + "</body></html>",
+                "text/html",
+                null);
         WebkitUtils.waitForFuture(imageLoaded.future());
-        getInstrumentation().waitForIdleSync();
+        getTestEnvironment().waitForIdleSync();
 
         final HrefCheckHandler handler = new HrefCheckHandler(mWebView.getHandler().getLooper());
         final Message msg = new Message();
@@ -1816,10 +1819,11 @@
         int middleY = location[1] + mOnUiThread.getWebView().getHeight() / 2;
 
         long time = SystemClock.uptimeMillis();
-        getInstrumentation().sendPointerSync(
-                MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN,
-                        middleX, middleY, 0));
-        getInstrumentation().waitForIdleSync();
+        getTestEnvironment()
+                .sendPointerSync(
+                        MotionEvent.obtain(
+                                time, time, MotionEvent.ACTION_DOWN, middleX, middleY, 0));
+        getTestEnvironment().waitForIdleSync();
         mOnUiThread.requestImageRef(msg);
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
@@ -1841,23 +1845,22 @@
         assertEquals(imgUrl, handler.mResultUrl);
     }
 
-    @UiThreadTest
+    @Test
     public void testDebugDump() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebView.debugDump();
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    mWebView.debugDump();
+                });
     }
 
+    @Test
     public void testGetHitTestResult() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        final String anchor = "<p><a href=\"" + TestHtmlConstants.EXT_WEB_URL1
-                + "\">normal anchor</a></p>";
+        final String anchor =
+                "<p><a href=\"" + TestHtmlConstants.EXT_WEB_URL1 + "\">normal anchor</a></p>";
         final String blankAnchor = "<p><a href=\"\">blank anchor</a></p>";
-        final String form = "<p><form><input type=\"text\" name=\"Test\"><br>"
-                + "<input type=\"submit\" value=\"Submit\"></form></p>";
+        final String form =
+                "<p><form><input type=\"text\" name=\"Test\"><br>"
+                        + "<input type=\"submit\" value=\"Submit\"></form></p>";
         String phoneNo = "3106984000";
         final String tel = "<p><a href=\"tel:" + phoneNo + "\">Phone</a></p>";
         String email = "test@gmail.com";
@@ -1865,10 +1868,20 @@
         String location = "shanghai";
         final String geo = "<p><a href=\"geo:0,0?q=" + location + "\">Location</a></p>";
 
-        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion("fake://home",
-                "<html><body>" + anchor + blankAnchor + form + tel + mailto +
-                geo + "</body></html>", "text/html", "UTF-8", null);
-        getInstrumentation().waitForIdleSync();
+        mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
+                "fake://home",
+                "<html><body>"
+                        + anchor
+                        + blankAnchor
+                        + form
+                        + tel
+                        + mailto
+                        + geo
+                        + "</body></html>",
+                "text/html",
+                "UTF-8",
+                null);
+        getTestEnvironment().waitForIdleSync();
 
         // anchor
         moveFocusDown();
@@ -1913,16 +1926,13 @@
         assertEquals(location, hitTestResult.getExtra());
     }
 
+    @Test
     public void testSetInitialScale() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         final String p = "<p style=\"height:1000px;width:1000px\">Test setInitialScale.</p>";
-        final float defaultScale =
-            getActivity().getResources().getDisplayMetrics().density;
+        final float defaultScale = mContext.getResources().getDisplayMetrics().density;
 
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + "</body></html>", "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + "</body></html>", "text/html", null);
 
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
@@ -1933,8 +1943,8 @@
 
         mOnUiThread.setInitialScale(0);
         // modify content to fool WebKit into re-loading
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + "2" + "</body></html>", "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + "2" + "</body></html>", "text/html", null);
 
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
@@ -1944,8 +1954,8 @@
         }.run();
 
         mOnUiThread.setInitialScale(50);
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + "3" + "</body></html>", "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + "3" + "</body></html>", "text/html", null);
 
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
@@ -1955,8 +1965,8 @@
         }.run();
 
         mOnUiThread.setInitialScale(0);
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + "4" + "</body></html>", "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + "4" + "</body></html>", "text/html", null);
 
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
@@ -1966,112 +1976,112 @@
         }.run();
     }
 
-    @UiThreadTest
+    @Test
     public void testClearHistory() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        startWebServer(false);
-        String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
-        String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
-        String url3 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL3);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
-        mOnUiThread.loadUrlAndWaitForCompletion(url1);
-        pollingCheckWebBackForwardList(url1, 0, 1);
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
+                    String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
+                    String url3 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL3);
 
-        mOnUiThread.loadUrlAndWaitForCompletion(url2);
-        pollingCheckWebBackForwardList(url2, 1, 2);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url1);
+                    pollingCheckWebBackForwardList(url1, 0, 1);
 
-        mOnUiThread.loadUrlAndWaitForCompletion(url3);
-        pollingCheckWebBackForwardList(url3, 2, 3);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url2);
+                    pollingCheckWebBackForwardList(url2, 1, 2);
 
-        mWebView.clearHistory();
+                    mOnUiThread.loadUrlAndWaitForCompletion(url3);
+                    pollingCheckWebBackForwardList(url3, 2, 3);
 
-        // only current URL is left after clearing
-        pollingCheckWebBackForwardList(url3, 0, 1);
+                    mWebView.clearHistory();
+
+                    // only current URL is left after clearing
+                    pollingCheckWebBackForwardList(url3, 0, 1);
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testSaveAndRestoreState() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        assertNull("Should return null when there's nothing to save",
-                mWebView.saveState(new Bundle()));
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
-        startWebServer(false);
-        String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
-        String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
-        String url3 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL3);
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    assertNull(
+                            "Should return null when there's nothing to save",
+                            mWebView.saveState(new Bundle()));
 
-        // make a history list
-        mOnUiThread.loadUrlAndWaitForCompletion(url1);
-        pollingCheckWebBackForwardList(url1, 0, 1);
-        mOnUiThread.loadUrlAndWaitForCompletion(url2);
-        pollingCheckWebBackForwardList(url2, 1, 2);
-        mOnUiThread.loadUrlAndWaitForCompletion(url3);
-        pollingCheckWebBackForwardList(url3, 2, 3);
+                    String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
+                    String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
+                    String url3 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL3);
 
-        // save the list
-        Bundle bundle = new Bundle();
-        WebBackForwardList saveList = mWebView.saveState(bundle);
-        assertNotNull(saveList);
-        assertEquals(3, saveList.getSize());
-        assertEquals(2, saveList.getCurrentIndex());
-        assertEquals(url1, saveList.getItemAtIndex(0).getUrl());
-        assertEquals(url2, saveList.getItemAtIndex(1).getUrl());
-        assertEquals(url3, saveList.getItemAtIndex(2).getUrl());
+                    // make a history list
+                    mOnUiThread.loadUrlAndWaitForCompletion(url1);
+                    pollingCheckWebBackForwardList(url1, 0, 1);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url2);
+                    pollingCheckWebBackForwardList(url2, 1, 2);
+                    mOnUiThread.loadUrlAndWaitForCompletion(url3);
+                    pollingCheckWebBackForwardList(url3, 2, 3);
 
-        // change the content to a new "blank" web view without history
-        final WebView newWebView = new WebView(getActivity());
+                    // save the list
+                    Bundle bundle = new Bundle();
+                    WebBackForwardList saveList = mWebView.saveState(bundle);
+                    assertNotNull(saveList);
+                    assertEquals(3, saveList.getSize());
+                    assertEquals(2, saveList.getCurrentIndex());
+                    assertEquals(url1, saveList.getItemAtIndex(0).getUrl());
+                    assertEquals(url2, saveList.getItemAtIndex(1).getUrl());
+                    assertEquals(url3, saveList.getItemAtIndex(2).getUrl());
 
-        WebBackForwardList copyListBeforeRestore = newWebView.copyBackForwardList();
-        assertNotNull(copyListBeforeRestore);
-        assertEquals(0, copyListBeforeRestore.getSize());
+                    // change the content to a new "blank" web view without history
+                    final WebView newWebView = new WebView(mContext);
 
-        // restore the list
-        final WebBackForwardList restoreList = newWebView.restoreState(bundle);
-        assertNotNull(restoreList);
-        assertEquals(3, restoreList.getSize());
-        assertEquals(2, saveList.getCurrentIndex());
+                    WebBackForwardList copyListBeforeRestore = newWebView.copyBackForwardList();
+                    assertNotNull(copyListBeforeRestore);
+                    assertEquals(0, copyListBeforeRestore.getSize());
 
-        // wait for the list items to get inflated
-        new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
-            @Override
-            protected boolean check() {
-                return restoreList.getItemAtIndex(0).getUrl() != null &&
-                       restoreList.getItemAtIndex(1).getUrl() != null &&
-                       restoreList.getItemAtIndex(2).getUrl() != null;
-            }
-        }.run();
-        assertEquals(url1, restoreList.getItemAtIndex(0).getUrl());
-        assertEquals(url2, restoreList.getItemAtIndex(1).getUrl());
-        assertEquals(url3, restoreList.getItemAtIndex(2).getUrl());
+                    // restore the list
+                    final WebBackForwardList restoreList = newWebView.restoreState(bundle);
+                    assertNotNull(restoreList);
+                    assertEquals(3, restoreList.getSize());
+                    assertEquals(2, saveList.getCurrentIndex());
 
-        WebBackForwardList copyListAfterRestore = newWebView.copyBackForwardList();
-        assertNotNull(copyListAfterRestore);
-        assertEquals(3, copyListAfterRestore.getSize());
-        assertEquals(2, copyListAfterRestore.getCurrentIndex());
-        assertEquals(url1, copyListAfterRestore.getItemAtIndex(0).getUrl());
-        assertEquals(url2, copyListAfterRestore.getItemAtIndex(1).getUrl());
-        assertEquals(url3, copyListAfterRestore.getItemAtIndex(2).getUrl());
+                    // wait for the list items to get inflated
+                    new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
+                        @Override
+                        protected boolean check() {
+                            return restoreList.getItemAtIndex(0).getUrl() != null
+                                    && restoreList.getItemAtIndex(1).getUrl() != null
+                                    && restoreList.getItemAtIndex(2).getUrl() != null;
+                        }
+                    }.run();
+                    assertEquals(url1, restoreList.getItemAtIndex(0).getUrl());
+                    assertEquals(url2, restoreList.getItemAtIndex(1).getUrl());
+                    assertEquals(url3, restoreList.getItemAtIndex(2).getUrl());
 
-        newWebView.destroy();
+                    WebBackForwardList copyListAfterRestore = newWebView.copyBackForwardList();
+                    assertNotNull(copyListAfterRestore);
+                    assertEquals(3, copyListAfterRestore.getSize());
+                    assertEquals(2, copyListAfterRestore.getCurrentIndex());
+                    assertEquals(url1, copyListAfterRestore.getItemAtIndex(0).getUrl());
+                    assertEquals(url2, copyListAfterRestore.getItemAtIndex(1).getUrl());
+                    assertEquals(url3, copyListAfterRestore.getItemAtIndex(2).getUrl());
+
+                    newWebView.destroy();
+                });
     }
 
+    @Test
     public void testRequestChildRectangleOnScreen() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         // It is needed to make test pass on some devices.
         mOnUiThread.setLayoutToMatchParent();
 
         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
         final int dimension = 2 * Math.max(metrics.widthPixels, metrics.heightPixels);
         String p = "<p style=\"height:" + dimension + "px;width:" + dimension + "px\">&nbsp;</p>";
-        mOnUiThread.loadDataAndWaitForCompletion("<html><body>" + p
-                + "</body></html>", "text/html", null);
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><body>" + p + "</body></html>", "text/html", null);
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
@@ -2089,11 +2099,8 @@
         assertThat(mOnUiThread.getScrollY(), greaterThan(origY));
     }
 
+    @Test
     public void testSetDownloadListener() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final SettableFuture<Void> downloadStartFuture = SettableFuture.create();
         final class MyDownloadListener implements DownloadListener {
             public String url;
@@ -2102,8 +2109,12 @@
             public String contentDisposition;
 
             @Override
-            public void onDownloadStart(String url, String userAgent, String contentDisposition,
-                    String mimetype, long contentLength) {
+            public void onDownloadStart(
+                    String url,
+                    String userAgent,
+                    String contentDisposition,
+                    String mimetype,
+                    long contentLength) {
                 this.url = url;
                 this.mimeType = mimetype;
                 this.contentLength = contentLength;
@@ -2116,7 +2127,7 @@
         final int length = 100;
         final MyDownloadListener listener = new MyDownloadListener();
 
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String url = mWebServer.getBinaryUrl(mimeType, length);
 
         // By default, WebView sends an intent to ask the system to
@@ -2127,9 +2138,10 @@
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         mOnUiThread.loadDataAndWaitForCompletion(
                 "<html><body onload=\"window.location = \'" + url + "\'\"></body></html>",
-                "text/html", null);
+                "text/html",
+                null);
         // Wait for layout to complete before setting focus.
-        getInstrumentation().waitForIdleSync();
+        getTestEnvironment().waitForIdleSync();
 
         WebkitUtils.waitForFuture(downloadStartFuture);
         assertEquals(url, listener.url);
@@ -2138,31 +2150,29 @@
         assertEquals(mimeType, listener.mimeType);
     }
 
-    @UiThreadTest
+    @Test
     public void testSetLayoutParams() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(600, 800);
-        mWebView.setLayoutParams(params);
-        assertSame(params, mWebView.getLayoutParams());
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(600, 800);
+                    mWebView.setLayoutParams(params);
+                    assertSame(params, mWebView.getLayoutParams());
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testSetMapTrackballToArrowKeys() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mWebView.setMapTrackballToArrowKeys(true);
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    mWebView.setMapTrackballToArrowKeys(true);
+                });
     }
 
+    @Test
     public void testSetNetworkAvailable() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         WebSettings settings = mOnUiThread.getSettings();
         settings.setJavaScriptEnabled(true);
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
 
         String url = mWebServer.getAssetUrl(TestHtmlConstants.NETWORK_STATE_URL);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
@@ -2189,42 +2199,39 @@
         }.run();
     }
 
+    @Test
     public void testSetWebChromeClient() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final SettableFuture<Void> future = SettableFuture.create();
-        mOnUiThread.setWebChromeClient(new WaitForProgressClient(mOnUiThread) {
-            @Override
-            public void onProgressChanged(WebView view, int newProgress) {
-                super.onProgressChanged(view, newProgress);
-                future.set(null);
-            }
-        });
-        getInstrumentation().waitForIdleSync();
+        mOnUiThread.setWebChromeClient(
+                new WaitForProgressClient(mOnUiThread) {
+                    @Override
+                    public void onProgressChanged(WebView view, int newProgress) {
+                        super.onProgressChanged(view, newProgress);
+                        future.set(null);
+                    }
+                });
+        getTestEnvironment().waitForIdleSync();
         assertFalse(future.isDone());
 
-        startWebServer(false);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
         mOnUiThread.loadUrlAndWaitForCompletion(url);
-        getInstrumentation().waitForIdleSync();
+        getTestEnvironment().waitForIdleSync();
 
         WebkitUtils.waitForFuture(future);
     }
 
+    @Test
     public void testPauseResumeTimers() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         class Monitor {
             private boolean mIsUpdated;
 
             @JavascriptInterface
             public synchronized void update() {
-                mIsUpdated  = true;
+                mIsUpdated = true;
                 notify();
             }
+
             public synchronized boolean waitForUpdate() {
                 while (!mIsUpdated) {
                     try {
@@ -2241,25 +2248,28 @@
                 mIsUpdated = false;
                 return true;
             }
-        };
+        }
+
         final Monitor monitor = new Monitor();
-        final String updateMonitorHtml = "<html>" +
-                "<body onload=\"monitor.update();\"></body></html>";
+        final String updateMonitorHtml =
+                "<html>" + "<body onload=\"monitor.update();\"></body></html>";
 
         // Test that JavaScript is executed even with timers paused.
-        WebkitUtils.onMainThreadSync(() -> {
-            mWebView.getSettings().setJavaScriptEnabled(true);
-            mWebView.addJavascriptInterface(monitor, "monitor");
-            mWebView.pauseTimers();
-            mOnUiThread.loadDataAndWaitForCompletion(updateMonitorHtml,
-                    "text/html", null);
-        });
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    mWebView.getSettings().setJavaScriptEnabled(true);
+                    mWebView.addJavascriptInterface(monitor, "monitor");
+                    mWebView.pauseTimers();
+                    mOnUiThread.loadDataAndWaitForCompletion(updateMonitorHtml, "text/html", null);
+                });
         assertTrue(monitor.waitForUpdate());
 
         // Start a timer and test that it does not fire.
         mOnUiThread.loadDataAndWaitForCompletion(
-                "<html><body onload='setTimeout(function(){monitor.update();},100)'>" +
-                "</body></html>", "text/html", null);
+                "<html><body onload='setTimeout(function(){monitor.update();},100)'>"
+                        + "</body></html>",
+                "text/html",
+                null);
         assertFalse(monitor.waitForUpdate());
 
         // Resume timers and test that the timer fires.
@@ -2268,37 +2278,31 @@
     }
 
     // verify query parameters can be passed correctly to android asset files
+    @Test
     public void testAndroidAssetQueryParam() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         WebSettings settings = mOnUiThread.getSettings();
         settings.setJavaScriptEnabled(true);
         // test passing a parameter
-        String fileUrl = TestHtmlConstants.getFileUrl(TestHtmlConstants.PARAM_ASSET_URL+"?val=SUCCESS");
+        String fileUrl =
+                TestHtmlConstants.getFileUrl(TestHtmlConstants.PARAM_ASSET_URL + "?val=SUCCESS");
         mOnUiThread.loadUrlAndWaitForCompletion(fileUrl);
         assertEquals("SUCCESS", mOnUiThread.getTitle());
     }
 
     // verify anchors work correctly for android asset files
+    @Test
     public void testAndroidAssetAnchor() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         WebSettings settings = mOnUiThread.getSettings();
         settings.setJavaScriptEnabled(true);
         // test using an anchor
-        String fileUrl = TestHtmlConstants.getFileUrl(TestHtmlConstants.ANCHOR_ASSET_URL+"#anchor");
+        String fileUrl =
+                TestHtmlConstants.getFileUrl(TestHtmlConstants.ANCHOR_ASSET_URL + "#anchor");
         mOnUiThread.loadUrlAndWaitForCompletion(fileUrl);
         assertEquals("anchor", mOnUiThread.getTitle());
     }
 
+    @Test
     public void testEvaluateJavascript() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         mOnUiThread.getSettings().setJavaScriptEnabled(true);
         mOnUiThread.loadUrlAndWaitForCompletion("about:blank");
 
@@ -2317,26 +2321,26 @@
     }
 
     // Verify Print feature can create a PDF file with a correct preamble.
+    @Test
     public void testPrinting() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        mOnUiThread.loadDataAndWaitForCompletion("<html><head></head>" +
-                "<body>foo</body></html>",
-                "text/html", null);
-        final PrintDocumentAdapter adapter =  mOnUiThread.createPrintDocumentAdapter();
+        mOnUiThread.loadDataAndWaitForCompletion(
+                "<html><head></head>" + "<body>foo</body></html>", "text/html", null);
+        final PrintDocumentAdapter adapter = mOnUiThread.createPrintDocumentAdapter();
         printDocumentStart(adapter);
-        PrintAttributes attributes = new PrintAttributes.Builder()
-                .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
-                .setResolution(new PrintAttributes.Resolution("foo", "bar", 300, 300))
-                .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
-                .build();
-        final WebViewCtsActivity activity = getActivity();
-        final File file = activity.getFileStreamPath(PRINTER_TEST_FILE);
-        final ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file,
-                ParcelFileDescriptor.parseMode("w"));
+        PrintAttributes attributes =
+                new PrintAttributes.Builder()
+                        .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
+                        .setResolution(new PrintAttributes.Resolution("foo", "bar", 300, 300))
+                        .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
+                        .build();
+        final File file = mContext.getFileStreamPath(PRINTER_TEST_FILE);
+        final ParcelFileDescriptor descriptor =
+                ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode("w"));
         final SettableFuture<Void> result = SettableFuture.create();
-        printDocumentLayout(adapter, null, attributes,
+        printDocumentLayout(
+                adapter,
+                null,
+                attributes,
                 new LayoutResultCallback() {
                     // Called on UI thread
                     @Override
@@ -2361,44 +2365,45 @@
     }
 
     // Verify Print feature can create a PDF file with correct number of pages.
+    @Test
     public void testPrintingPagesCount() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         String content = "<html><head></head><body>";
         for (int i = 0; i < 500; ++i) {
             content += "<br />abcdefghijk<br />";
         }
         content += "</body></html>";
         mOnUiThread.loadDataAndWaitForCompletion(content, "text/html", null);
-        final PrintDocumentAdapter adapter =  mOnUiThread.createPrintDocumentAdapter();
+        final PrintDocumentAdapter adapter = mOnUiThread.createPrintDocumentAdapter();
         printDocumentStart(adapter);
-        PrintAttributes attributes = new PrintAttributes.Builder()
-                .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
-                .setResolution(new PrintAttributes.Resolution("foo", "bar", 300, 300))
-                .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
-                .build();
-        final WebViewCtsActivity activity = getActivity();
-        final File file = activity.getFileStreamPath(PRINTER_TEST_FILE);
-        final ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file,
-                ParcelFileDescriptor.parseMode("w"));
+        PrintAttributes attributes =
+                new PrintAttributes.Builder()
+                        .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
+                        .setResolution(new PrintAttributes.Resolution("foo", "bar", 300, 300))
+                        .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
+                        .build();
+        final File file = mContext.getFileStreamPath(PRINTER_TEST_FILE);
+        final ParcelFileDescriptor descriptor =
+                ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode("w"));
         final SettableFuture<Void> result = SettableFuture.create();
-        printDocumentLayout(adapter, null, attributes,
+        printDocumentLayout(
+                adapter,
+                null,
+                attributes,
                 new LayoutResultCallback() {
                     // Called on UI thread
                     @Override
                     public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
-                        PageRange[] pageRanges = new PageRange[] {
-                            new PageRange(1, 1), new PageRange(4, 7)
-                        };
+                        PageRange[] pageRanges =
+                                new PageRange[] {new PageRange(1, 1), new PageRange(4, 7)};
                         savePrintedPage(adapter, descriptor, pageRanges, result);
                     }
                 });
         try {
             WebkitUtils.waitForFuture(result);
             assertThat(file.length(), greaterThan(0L));
-            PdfRenderer renderer = new PdfRenderer(
-                ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY));
+            PdfRenderer renderer =
+                    new PdfRenderer(
+                            ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY));
             assertEquals(5, renderer.getPageCount());
         } finally {
             descriptor.close();
@@ -2411,34 +2416,35 @@
      * androidx.webkit.WebViewCompatTest#testVisualStateCallbackCalled. Modifications to this test
      * should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testVisualStateCallbackCalled() throws Exception {
         // Check that the visual state callback is called correctly.
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final long kRequest = 100;
 
         mOnUiThread.loadUrl("about:blank");
 
         final SettableFuture<Long> visualStateFuture = SettableFuture.create();
-        mOnUiThread.postVisualStateCallback(kRequest, new VisualStateCallback() {
-            public void onComplete(long requestId) {
-                visualStateFuture.set(requestId);
-            }
-        });
+        mOnUiThread.postVisualStateCallback(
+                kRequest,
+                new VisualStateCallback() {
+                    public void onComplete(long requestId) {
+                        visualStateFuture.set(requestId);
+                    }
+                });
 
         assertEquals(kRequest, (long) WebkitUtils.waitForFuture(visualStateFuture));
     }
 
     private static boolean setSafeBrowsingAllowlistSync(List<String> allowlist) {
         final SettableFuture<Boolean> safeBrowsingAllowlistFuture = SettableFuture.create();
-        WebView.setSafeBrowsingWhitelist(allowlist, new ValueCallback<Boolean>() {
-            @Override
-            public void onReceiveValue(Boolean success) {
-                safeBrowsingAllowlistFuture.set(success);
-            }
-        });
+        WebView.setSafeBrowsingWhitelist(
+                allowlist,
+                new ValueCallback<Boolean>() {
+                    @Override
+                    public void onReceiveValue(Boolean success) {
+                        safeBrowsingAllowlistFuture.set(success);
+                    }
+                });
         return WebkitUtils.waitForFuture(safeBrowsingAllowlistFuture);
     }
 
@@ -2448,11 +2454,8 @@
      * Modifications to this test should be reflected in that test as necessary. See
      * http://go/modifying-webview-cts.
      */
+    @Test
     public void testSetSafeBrowsingAllowlistWithMalformedList() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         List allowlist = new ArrayList<String>();
         // Protocols are not supported in the allowlist
         allowlist.add("http://google.com");
@@ -2461,33 +2464,35 @@
 
     /**
      * This should remain functionally equivalent to
-     * androidx.webkit.WebViewCompatTest#testSetSafeBrowsingAllowlistWithValidList. Modifications
-     * to this test should be reflected in that test as necessary. See
-     * http://go/modifying-webview-cts.
+     * androidx.webkit.WebViewCompatTest#testSetSafeBrowsingAllowlistWithValidList. Modifications to
+     * this test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testSetSafeBrowsingAllowlistWithValidList() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         List allowlist = new ArrayList<String>();
         allowlist.add("safe-browsing");
         assertTrue("Valid allowlist should be successful", setSafeBrowsingAllowlistSync(allowlist));
 
         final SettableFuture<Void> pageFinishedFuture = SettableFuture.create();
-        mOnUiThread.setWebViewClient(new WebViewClient() {
-            @Override
-            public void onPageFinished(WebView view, String url) {
-                pageFinishedFuture.set(null);
-            }
+        mOnUiThread.setWebViewClient(
+                new WebViewClient() {
+                    @Override
+                    public void onPageFinished(WebView view, String url) {
+                        pageFinishedFuture.set(null);
+                    }
 
-            @Override
-            public void onSafeBrowsingHit(WebView view, WebResourceRequest request, int threatType,
-                    SafeBrowsingResponse callback) {
-                pageFinishedFuture.setException(new IllegalStateException(
-                        "Should not invoke onSafeBrowsingHit for " + request.getUrl()));
-            }
-        });
+                    @Override
+                    public void onSafeBrowsingHit(
+                            WebView view,
+                            WebResourceRequest request,
+                            int threatType,
+                            SafeBrowsingResponse callback) {
+                        pageFinishedFuture.setException(
+                                new IllegalStateException(
+                                        "Should not invoke onSafeBrowsingHit for "
+                                                + request.getUrl()));
+                    }
+                });
 
         mOnUiThread.loadUrl("chrome://safe-browsing/match?type=malware");
 
@@ -2500,24 +2505,24 @@
      * androidx.webkit.WebViewCompatTest#testGetWebViewClient. Modifications to this test should be
      * reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
-    @UiThreadTest
+    @Test
     public void testGetWebViewClient() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    // getWebViewClient should return a default WebViewClient if it hasn't been set
+                    // yet
+                    WebView webView = new WebView(mContext);
+                    WebViewClient client = webView.getWebViewClient();
+                    assertNotNull(client);
+                    assertTrue(client instanceof WebViewClient);
 
-        // getWebViewClient should return a default WebViewClient if it hasn't been set yet
-        WebView webView = new WebView(getActivity());
-        WebViewClient client = webView.getWebViewClient();
-        assertNotNull(client);
-        assertTrue(client instanceof WebViewClient);
-
-        // getWebViewClient should return the client after it has been set
-        WebViewClient client2 = new WebViewClient();
-        assertNotSame(client, client2);
-        webView.setWebViewClient(client2);
-        assertSame(client2, webView.getWebViewClient());
-        webView.destroy();
+                    // getWebViewClient should return the client after it has been set
+                    WebViewClient client2 = new WebViewClient();
+                    assertNotSame(client, client2);
+                    webView.setWebViewClient(client2);
+                    assertSame(client2, webView.getWebViewClient());
+                    webView.destroy();
+                });
     }
 
     /**
@@ -2525,56 +2530,48 @@
      * androidx.webkit.WebViewCompatTest#testGetWebChromeClient. Modifications to this test should
      * be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
-    @UiThreadTest
+    @Test
     public void testGetWebChromeClient() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    // getWebChromeClient should return null if the client hasn't been set yet
+                    WebView webView = new WebView(mContext);
+                    WebChromeClient client = webView.getWebChromeClient();
+                    assertNull(client);
 
-        // getWebChromeClient should return null if the client hasn't been set yet
-        WebView webView = new WebView(getActivity());
-        WebChromeClient client = webView.getWebChromeClient();
-        assertNull(client);
-
-        // getWebChromeClient should return the client after it has been set
-        WebChromeClient client2 = new WebChromeClient();
-        assertNotSame(client, client2);
-        webView.setWebChromeClient(client2);
-        assertSame(client2, webView.getWebChromeClient());
-        webView.destroy();
+                    // getWebChromeClient should return the client after it has been set
+                    WebChromeClient client2 = new WebChromeClient();
+                    assertNotSame(client, client2);
+                    webView.setWebChromeClient(client2);
+                    assertSame(client2, webView.getWebChromeClient());
+                    webView.destroy();
+                });
     }
 
-    @UiThreadTest
+    @Test
     public void testSetCustomTextClassifier() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         class CustomTextClassifier implements TextClassifier {
             @Override
             public TextSelection suggestSelection(
-                CharSequence text,
-                int startIndex,
-                int endIndex,
-                LocaleList defaultLocales) {
+                    CharSequence text, int startIndex, int endIndex, LocaleList defaultLocales) {
                 return new TextSelection.Builder(0, 1).build();
             }
 
             @Override
             public TextClassification classifyText(
-                CharSequence text,
-                int startIndex,
-                int endIndex,
-                LocaleList defaultLocales) {
+                    CharSequence text, int startIndex, int endIndex, LocaleList defaultLocales) {
                 return new TextClassification.Builder().build();
             }
-        };
+        }
 
-        TextClassifier classifier = new CustomTextClassifier();
-        WebView webView = new WebView(getActivity());
-        webView.setTextClassifier(classifier);
-        assertSame(webView.getTextClassifier(), classifier);
-        webView.destroy();
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    TextClassifier classifier = new CustomTextClassifier();
+                    WebView webView = new WebView(mContext);
+                    webView.setTextClassifier(classifier);
+                    assertSame(webView.getTextClassifier(), classifier);
+                    webView.destroy();
+                });
     }
 
     private static class MockContext extends ContextWrapper {
@@ -2596,23 +2593,25 @@
 
     /**
      * This should remain functionally equivalent to
-     * androidx.webkit.WebViewCompatTest#testStartSafeBrowsingUseApplicationContext. Modifications to
-     * this test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
+     * androidx.webkit.WebViewCompatTest#testStartSafeBrowsingUseApplicationContext. Modifications
+     * to this test should be reflected in that test as necessary. See
+     * http://go/modifying-webview-cts.
      */
+    @Test
     public void testStartSafeBrowsingUseApplicationContext() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        final MockContext ctx = new MockContext(getActivity());
+        final MockContext ctx =
+                new MockContext(
+                        ApplicationProvider.getApplicationContext().getApplicationContext());
         final SettableFuture<Boolean> startSafeBrowsingFuture = SettableFuture.create();
-        WebView.startSafeBrowsing(ctx, new ValueCallback<Boolean>() {
-            @Override
-            public void onReceiveValue(Boolean value) {
-                startSafeBrowsingFuture.set(ctx.wasGetApplicationContextCalled());
-                return;
-            }
-        });
+        WebView.startSafeBrowsing(
+                ctx,
+                new ValueCallback<Boolean>() {
+                    @Override
+                    public void onReceiveValue(Boolean value) {
+                        startSafeBrowsingFuture.set(ctx.wasGetApplicationContextCalled());
+                        return;
+                    }
+                });
         assertTrue(WebkitUtils.waitForFuture(startSafeBrowsingFuture));
     }
 
@@ -2622,40 +2621,40 @@
      * Modifications to this test should be reflected in that test as necessary. See
      * http://go/modifying-webview-cts.
      */
+    @Test
     public void testStartSafeBrowsingWithNullCallbackDoesntCrash() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
-        WebView.startSafeBrowsing(getActivity().getApplicationContext(), null);
+        WebView.startSafeBrowsing(
+                ApplicationProvider.getApplicationContext().getApplicationContext(), null);
     }
 
     /**
      * This should remain functionally equivalent to
-     * androidx.webkit.WebViewCompatTest#testStartSafeBrowsingInvokesCallback. Modifications to
-     * this test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
+     * androidx.webkit.WebViewCompatTest#testStartSafeBrowsingInvokesCallback. Modifications to this
+     * test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testStartSafeBrowsingInvokesCallback() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         final SettableFuture<Boolean> startSafeBrowsingFuture = SettableFuture.create();
-        WebView.startSafeBrowsing(getActivity().getApplicationContext(),
+        WebView.startSafeBrowsing(
+                ApplicationProvider.getApplicationContext().getApplicationContext(),
                 new ValueCallback<Boolean>() {
-            @Override
-            public void onReceiveValue(Boolean value) {
-                startSafeBrowsingFuture.set(Looper.getMainLooper().isCurrentThread());
-                return;
-            }
-        });
+                    @Override
+                    public void onReceiveValue(Boolean value) {
+                        startSafeBrowsingFuture.set(Looper.getMainLooper().isCurrentThread());
+                        return;
+                    }
+                });
         assertTrue(WebkitUtils.waitForFuture(startSafeBrowsingFuture));
     }
 
-    private void savePrintedPage(final PrintDocumentAdapter adapter,
-            final ParcelFileDescriptor descriptor, final PageRange[] pageRanges,
+    private void savePrintedPage(
+            final PrintDocumentAdapter adapter,
+            final ParcelFileDescriptor descriptor,
+            final PageRange[] pageRanges,
             final SettableFuture<Void> result) {
-        adapter.onWrite(pageRanges, descriptor,
+        adapter.onWrite(
+                pageRanges,
+                descriptor,
                 new CancellationSignal(),
                 new WriteResultCallback() {
                     @Override
@@ -2671,18 +2670,26 @@
     }
 
     private void printDocumentStart(final PrintDocumentAdapter adapter) {
-        WebkitUtils.onMainThreadSync(() -> {
-            adapter.onStart();
-        });
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    adapter.onStart();
+                });
     }
 
-    private void printDocumentLayout(final PrintDocumentAdapter adapter,
-            final PrintAttributes oldAttributes, final PrintAttributes newAttributes,
+    private void printDocumentLayout(
+            final PrintDocumentAdapter adapter,
+            final PrintAttributes oldAttributes,
+            final PrintAttributes newAttributes,
             final LayoutResultCallback layoutResultCallback) {
-        WebkitUtils.onMainThreadSync(() -> {
-            adapter.onLayout(oldAttributes, newAttributes, new CancellationSignal(),
-                    layoutResultCallback, null);
-        });
+        WebkitUtils.onMainThreadSync(
+                () -> {
+                    adapter.onLayout(
+                            oldAttributes,
+                            newAttributes,
+                            new CancellationSignal(),
+                            layoutResultCallback,
+                            null);
+                });
     }
 
     private static class HrefCheckHandler extends Handler {
@@ -2702,7 +2709,7 @@
             return mResultUrl;
         }
 
-        public void reset(){
+        public void reset() {
             mResultUrl = null;
             mHadRecieved = false;
         }
@@ -2716,13 +2723,13 @@
 
     private void moveFocusDown() throws Throwable {
         // send down key and wait for idle
-        getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
+        getTestEnvironment().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
         // waiting for idle isn't always sufficient for the key to be fully processed
         Thread.sleep(500);
     }
 
-    private void pollingCheckWebBackForwardList(final String currUrl, final int currIndex,
-            final int size) {
+    private void pollingCheckWebBackForwardList(
+            final String currUrl, final int currIndex, final int size) {
         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
             @Override
             protected boolean check() {
@@ -2732,8 +2739,8 @@
         }.run();
     }
 
-    private boolean checkWebBackForwardList(WebBackForwardList list, String currUrl,
-            int currIndex, int size) {
+    private boolean checkWebBackForwardList(
+            WebBackForwardList list, String currUrl, int currIndex, int size) {
         return (list != null)
                 && (list.getSize() == size)
                 && (list.getCurrentIndex() == currIndex)
@@ -2742,8 +2749,7 @@
 
     private void assertGoBackOrForwardBySteps(boolean expected, int steps) {
         // skip if steps equals to 0
-        if (steps == 0)
-            return;
+        if (steps == 0) return;
 
         int start = steps > 0 ? 1 : steps;
         int end = steps > 0 ? steps : -1;
@@ -2762,15 +2768,14 @@
     }
 
     private boolean isPictureFilledWithColor(Picture picture, int color) {
-        if (picture.getWidth() == 0 || picture.getHeight() == 0)
-            return false;
+        if (picture.getWidth() == 0 || picture.getHeight() == 0) return false;
 
-        Bitmap bitmap = Bitmap.createBitmap(picture.getWidth(), picture.getHeight(),
-                Config.ARGB_8888);
+        Bitmap bitmap =
+                Bitmap.createBitmap(picture.getWidth(), picture.getHeight(), Config.ARGB_8888);
         picture.draw(new Canvas(bitmap));
 
-        for (int i = 0; i < bitmap.getWidth(); i ++) {
-            for (int j = 0; j < bitmap.getHeight(); j ++) {
+        for (int i = 0; i < bitmap.getWidth(); i++) {
+            for (int j = 0; j < bitmap.getHeight(); j++) {
                 if (color != bitmap.getPixel(i, j)) {
                     return false;
                 }
@@ -2780,14 +2785,13 @@
     }
 
     /**
-     * Waits at least MIN_SCROLL_WAIT_MS for scrolling to start. Once started,
-     * scrolling is checked every SCROLL_WAIT_INTERVAL_MS for changes. Once
-     * changes have stopped, the function exits. If no scrolling has happened
-     * then the function exits after MIN_SCROLL_WAIT milliseconds.
+     * Waits at least MIN_SCROLL_WAIT_MS for scrolling to start. Once started, scrolling is checked
+     * every SCROLL_WAIT_INTERVAL_MS for changes. Once changes have stopped, the function exits. If
+     * no scrolling has happened then the function exits after MIN_SCROLL_WAIT milliseconds.
+     *
      * @param previousScrollY The Y scroll position prior to waiting.
      */
-    private void waitForScrollingComplete(int previousScrollY)
-            throws InterruptedException {
+    private void waitForScrollingComplete(int previousScrollY) throws InterruptedException {
         int scrollY = previousScrollY;
         // wait at least MIN_SCROLL_WAIT for something to happen.
         long noChangeMinWait = SystemClock.uptimeMillis() + MIN_SCROLL_WAIT_MS;
@@ -2809,11 +2813,8 @@
      * androidx.webkit.WebViewCompatTest#testGetSafeBrowsingPrivacyPolicyUrl. Modifications to this
      * test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
      */
+    @Test
     public void testGetSafeBrowsingPrivacyPolicyUrl() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         assertNotNull(WebView.getSafeBrowsingPrivacyPolicyUrl());
         try {
             new URL(WebView.getSafeBrowsingPrivacyPolicyUrl().toString());
@@ -2822,11 +2823,8 @@
         }
     }
 
+    @Test
     public void testWebViewClassLoaderReturnsNonNull() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         assertNotNull(WebView.getWebViewClassLoader());
     }
 }
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewTransportTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewTransportTest.java
index b35f7bc..1f159dc 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewTransportTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewTransportTest.java
@@ -16,31 +16,60 @@
 
 package android.webkit.cts;
 
-import android.test.ActivityInstrumentationTestCase2;
-import android.test.UiThreadTest;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
 import android.webkit.WebView;
 import android.webkit.WebView.WebViewTransport;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 
-public class WebViewTransportTest
-        extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
-    public WebViewTransportTest() {
-        super("android.webkit.cts", WebViewCtsActivity.class);
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewTransportTest extends SharedWebViewTest {
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            WebView webView = ((WebViewCtsActivity) activity).getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                    activity))
+                                    .setWebView(webView);
+                        });
+
+        return builder.build();
     }
 
-    @UiThreadTest
+    @Test
     public void testAccessWebView() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-        WebView webView = getActivity().getWebView();
-        WebViewTransport transport = webView.new WebViewTransport();
+        WebkitUtils.onMainThreadSync(() -> {
+            WebView webView = getTestEnvironment().getWebView();
+            WebViewTransport transport = webView.new WebViewTransport();
 
-        assertNull(transport.getWebView());
+            assertNull(transport.getWebView());
 
-        transport.setWebView(webView);
-        assertSame(webView, transport.getWebView());
+            transport.setWebView(webView);
+            assertSame(webView, transport.getWebView());
+        });
     }
 }
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebViewZoomTest.java b/tests/tests/webkit/src/android/webkit/cts/WebViewZoomTest.java
index dc160b2..6b8ea8d 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebViewZoomTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebViewZoomTest.java
@@ -21,22 +21,29 @@
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.Matchers.lessThan;
 import static org.hamcrest.Matchers.lessThanOrEqualTo;
-import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.*;
 
 import android.net.http.SslError;
-import android.os.StrictMode;
-import android.os.StrictMode.ThreadPolicy;
 import android.platform.test.annotations.AppModeFull;
-import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.SslErrorHandler;
 import android.webkit.WebSettings;
 import android.webkit.WebView;
-import android.webkit.WebViewClient;
 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
 
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
 import com.android.compatibility.common.util.NullWebViewUtils;
 import com.android.compatibility.common.util.PollingCheck;
 
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 
@@ -44,97 +51,104 @@
  *  Test WebView zooming behaviour
  */
 @AppModeFull
-public class WebViewZoomTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class WebViewZoomTest extends SharedWebViewTest{
+    @Rule
+    public ActivityScenarioRule mActivityScenarioRule =
+            new ActivityScenarioRule(WebViewCtsActivity.class);
+
+    private WebViewCtsActivity mActivity;
     private WebView mWebView;
     private WebViewOnUiThread mOnUiThread;
     private ScaleChangedWebViewClient mWebViewClient;
-    private CtsTestServer mWebServer;
+    private SharedSdkWebServer mWebServer;
 
     /**
      * Epsilon used in page scale value comparisons.
      */
     private static final float PAGE_SCALE_EPSILON = 0.0001f;
 
-    public WebViewZoomTest() {
-        super("com.android.cts.webkit", WebViewCtsActivity.class);
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        final WebViewCtsActivity activity = getActivity();
-        mWebView = activity.getWebView();
-        if (mWebView == null)
-            return;
+    @Before
+    public void setUp() throws Exception {
+        mWebView = getTestEnvironment().getWebView();
         mOnUiThread = new WebViewOnUiThread(mWebView);
-        mOnUiThread.requestFocus();
-
-        new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
-            @Override
-                protected boolean check() {
-                    return activity.hasWindowFocus();
-            }
-        }.run();
-
         mWebViewClient = new ScaleChangedWebViewClient();
         mOnUiThread.setWebViewClient(mWebViewClient);
-
         // Pinch zoom is not supported in wrap_content layouts.
         mOnUiThread.setLayoutHeightToMatchParent();
-
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void cleanup() throws Exception {
         if (mOnUiThread != null) {
             mOnUiThread.cleanUp();
         }
         if (mWebServer != null) {
-            stopWebServer();
+            mWebServer.shutdown();
         }
-        super.tearDown();
+
+        mActivity = null;
     }
 
-    private void stopWebServer() throws Exception {
-        assertNotNull(mWebServer);
-        ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
-        ThreadPolicy tmpPolicy = new ThreadPolicy.Builder(oldPolicy)
-                .permitNetwork()
-                .build();
-        StrictMode.setThreadPolicy(tmpPolicy);
-        mWebServer.shutdown();
-        mWebServer = null;
-        StrictMode.setThreadPolicy(oldPolicy);
+    @Override
+    protected SharedWebViewTestEnvironment createTestEnvironment() {
+        Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
+
+        SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
+
+        mActivityScenarioRule
+                .getScenario()
+                .onActivity(
+                        activity -> {
+                            mActivity = (WebViewCtsActivity) activity;
+
+                            WebView webView = mActivity.getWebView();
+                            builder.setHostAppInvoker(
+                                            SharedWebViewTestEnvironment.createHostAppInvoker(
+                                                    mActivity))
+                                    .setContext(mActivity)
+                                    .setWebView(webView);
+                        });
+
+        SharedWebViewTestEnvironment environment = builder.build();
+
+        // Wait for window focus and clean up the snapshot before
+        // returning the test environment.
+        if (environment.getWebView() != null) {
+            new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
+                @Override
+                protected boolean check() {
+                    return mActivity.hasWindowFocus();
+                }
+            }.run();
+        }
+
+        return environment;
     }
 
     private void setUpPage() throws Exception {
         assertFalse("onScaleChanged has already been called before page has been setup",
                 mWebViewClient.onScaleChangedCalled());
         assertNull(mWebServer);
-        // Pass CtsTestserver.SslMode.TRUST_ANY_CLIENT to make the server serve https URLs yet do
+        // Pass SslMode.TRUST_ANY_CLIENT to make the server serve https URLs yet do
         // not ask client for client authentication.
-        mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.TRUST_ANY_CLIENT);
+        mWebServer = getTestEnvironment().getSetupWebServer(SslMode.TRUST_ANY_CLIENT);
         mOnUiThread.loadUrlAndWaitForCompletion(
                 mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL));
         pollingCheckForCanZoomIn();
     }
 
+    @Test
     public void testZoomIn() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         setUpPage();
 
         assertTrue(mOnUiThread.zoomIn());
         mWebViewClient.waitForNextScaleChange();
     }
 
-    @SuppressWarnings("deprecation")
+    @Test
     public void testGetZoomControls() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         WebSettings settings = mOnUiThread.getSettings();
         assertTrue(settings.supportZoom());
         assertNotNull(
@@ -149,20 +163,16 @@
                 WebkitUtils.onMainThreadSync(() -> { return mWebView.getZoomControls(); }));
     }
 
+    @Test
     public void testInvokeZoomPicker() throws Exception {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         WebSettings settings = mOnUiThread.getSettings();
         assertTrue(settings.supportZoom());
         setUpPage();
         WebkitUtils.onMainThreadSync(() -> mWebView.invokeZoomPicker());
     }
 
+    @Test
     public void testZoom_canNotZoomInPastMaximum() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         float currScale = mOnUiThread.getScale();
         // Zoom in until maximum scale, in default increments.
         while (mOnUiThread.zoomIn()) {
@@ -173,10 +183,8 @@
         assertNoScaleChange(currScale);
     }
 
+    @Test
     public void testZoom_canNotZoomOutPastMinimum() {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         float currScale = mOnUiThread.getScale();
         // Zoom in until maximum scale, in default increments.
         while (mOnUiThread.zoomOut()) {
@@ -187,12 +195,9 @@
         assertNoScaleChange(currScale);
     }
 
+    @Test
     public void testCanZoomWhileZoomSupportedFalse() throws Throwable {
         // setZoomSupported disables user controls, but not zooming via API
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         setUpPage();
 
         WebSettings settings = mOnUiThread.getSettings();
@@ -210,12 +215,9 @@
         currScale = mWebViewClient.expectZoomOut(currScale);
     }
 
+    @Test
     public void testZoomByPowerOfTwoIncrements() throws Throwable {
         // setZoomSupported disables user controls, but not zooming via API
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         setUpPage();
         float currScale = mOnUiThread.getScale();
 
@@ -226,11 +228,8 @@
         currScale = mWebViewClient.expectZoomBy(currScale, 0.75f);
     }
 
+    @Test
     public void testZoomByNonPowerOfTwoIncrements() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
-
         setUpPage();
 
         float currScale = mOnUiThread.getScale();
@@ -258,10 +257,8 @@
         assertNoScaleChange(currScale);
     }
 
+    @Test
     public void testScaleChangeCallbackMatchesGetScale() throws Throwable {
-        if (!NullWebViewUtils.isWebViewAvailable()) {
-            return;
-        }
         assertFalse("There is an onScaleChanged before we call setUpPage()",
                 mWebViewClient.onScaleChangedCalled());
 
@@ -274,7 +271,7 @@
         ScaleChangedState state = mWebViewClient.waitForNextScaleChange();
         assertEquals(
                 "Expected onScaleChanged arg 2 (new scale) to equal view.getScale()",
-                state.mNewScale, mOnUiThread.getScale());
+                state.mNewScale, mOnUiThread.getScale(), 0);
     }
 
     private void assertNoScaleChange(float currScale) {
@@ -284,7 +281,7 @@
             Thread.sleep(500);
             assertFalse("There is an onScaleChanged left over from previous scale change",
                     mWebViewClient.onScaleChangedCalled());
-            assertEquals(currScale, mOnUiThread.getScale());
+            assertEquals(currScale, mOnUiThread.getScale(), 0);
         } catch (InterruptedException e) {
             fail("Interrupted");
         }
@@ -332,7 +329,7 @@
 
             float nextScale = currentScale * scaleAmount;
             ScaleChangedState state = waitForNextScaleChange();
-            assertEquals(currentScale, state.mOldScale);
+            assertEquals(currentScale, state.mOldScale, 0);
 
 
             // Zoom scale changes can come in multiple steps and the initial scale may have
@@ -373,14 +370,14 @@
 
         public float expectZoomOut(float currentScale) {
             ScaleChangedState state = waitForNextScaleChange();
-            assertEquals(currentScale, state.mOldScale);
+            assertEquals(currentScale, state.mOldScale, 0);
             assertThat(state.mNewScale, lessThan(currentScale));
             return state.mNewScale;
         }
 
         public float expectZoomIn(float currentScale) {
             ScaleChangedState state = waitForNextScaleChange();
-            assertEquals(currentScale, state.mOldScale);
+            assertEquals(currentScale, state.mOldScale, 0);
             assertThat(state.mNewScale, greaterThan(currentScale));
             return state.mNewScale;
         }
diff --git a/tests/tests/wifi/src/android/net/wifi/aware/cts/SingleDeviceTest.java b/tests/tests/wifi/src/android/net/wifi/aware/cts/SingleDeviceTest.java
index 8e4d380..c754d3d9 100644
--- a/tests/tests/wifi/src/android/net/wifi/aware/cts/SingleDeviceTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/aware/cts/SingleDeviceTest.java
@@ -630,7 +630,7 @@
      * then the attach/destroy will not correspond to enable/disable and will not result in a new
      * MAC address being generated.
      */
-    public void testAttachDiscoveryAddressChanges() {
+    public void testAttachDiscoveryAddressChanges() throws InterruptedException {
         if (!TestUtils.shouldTestWifiAware(getContext())) {
             return;
         }
@@ -639,6 +639,7 @@
         Set<TestUtils.MacWrapper> macs = new HashSet<>();
 
         for (int i = 0; i < numIterations; ++i) {
+            Thread.sleep(1000);
             AttachCallbackTest attachCb = new AttachCallbackTest();
             IdentityChangedListenerTest identityL = new IdentityChangedListenerTest();
             mWifiAwareManager.attach(attachCb, identityL, mHandler);
diff --git a/tests/tests/wifi/src/android/net/wifi/cts/WifiBackupRestoreTest.java b/tests/tests/wifi/src/android/net/wifi/cts/WifiBackupRestoreTest.java
index bce7ccb..0107119 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiBackupRestoreTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiBackupRestoreTest.java
@@ -27,6 +27,7 @@
 import static org.junit.Assume.assumeTrue;
 
 import android.app.UiAutomation;
+import android.app.compat.CompatChanges;
 import android.content.Context;
 import android.net.IpConfiguration;
 import android.net.LinkAddress;
@@ -91,6 +92,7 @@
     public static final int EXPECTED_LEGACY_STATIC_PROXY_PORT = 8000;
     public static final String EXPECTED_LEGACY_STATIC_PROXY_EXCLUSION_LIST = "";
     public static final String EXPECTED_LEGACY_PAC_PROXY_LOCATION = "http://";
+    private static final long NOT_OVERRIDE_EXISTING_NETWORKS_ON_RESTORE = 234793325L;
 
     private Context mContext;
     private WifiManager mWifiManager;
@@ -202,6 +204,7 @@
                         + "holding OVERRIDE_WIFI_CONFIG permission to fully evaluate the "
                         + "functionality");
             } else {
+
                 // Retrieve backup data.
                 byte[] backupData = mWifiManager.retrieveBackupData();
                 // Modify the metered bit.
@@ -219,14 +222,23 @@
 
                 // Restore the original backup data & ensure that the metered bit is back to orig.
                 mWifiManager.restoreBackupData(backupData);
-                int metered = mWifiManager.getConfiguredNetworks()
-                        .stream()
-                        .filter(n -> n.SSID.equals(origNetworkSsid))
-                        .findAny()
-                        .get().meteredOverride;
-                // Adopt two behaviors
-                assertThat(metered == origNetwork.meteredOverride
-                        || metered == modNetwork.meteredOverride).isTrue();
+
+                if (CompatChanges.isChangeEnabled(NOT_OVERRIDE_EXISTING_NETWORKS_ON_RESTORE)) {
+                    assertThat(mWifiManager.getConfiguredNetworks()
+                            .stream()
+                            .filter(n -> n.SSID.equals(origNetworkSsid))
+                            .findAny()
+                            .get().meteredOverride)
+                            .isNotEqualTo(origNetwork.meteredOverride);
+
+                } else {
+                    assertThat(mWifiManager.getConfiguredNetworks()
+                            .stream()
+                            .filter(n -> n.SSID.equals(origNetworkSsid))
+                            .findAny()
+                            .get().meteredOverride)
+                            .isEqualTo(origNetwork.meteredOverride);
+                }
             }
         } finally {
             // Restore the orig network
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 b4b113f..fd4a9b0 100644
--- a/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
+++ b/tests/tests/wifi/src/android/net/wifi/cts/WifiManagerTest.java
@@ -99,7 +99,6 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.compatibility.common.util.ApiLevelUtil;
-import com.android.compatibility.common.util.ApiTest;
 import com.android.compatibility.common.util.FeatureUtil;
 import com.android.compatibility.common.util.PollingCheck;
 import com.android.compatibility.common.util.PropertyUtil;
@@ -5534,7 +5533,6 @@
     /**
      * Tests {@link WifiConfiguration#setBssidAllowlist(List)}.
      */
-    @ApiTest(apis = "android.net.wifi.WifiConfiguration#setBssidAllowlist")
     public void testBssidAllowlist() throws Exception {
         if (!WifiFeature.isWifiSupported(getContext())) {
             // skip the test if WiFi is not supported
diff --git a/tests/uwb/AndroidTest.xml b/tests/uwb/AndroidTest.xml
deleted file mode 100644
index 3104ec5..0000000
--- a/tests/uwb/AndroidTest.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<configuration description="Config for CTS UWB test cases">
-    <option name="test-suite-tag" value="cts" />
-    <option name="config-descriptor:metadata" key="component" value="framework" />
-    <option name="config-descriptor:metadata" key="parameter" value="instant_app" />
-    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
-    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
-    <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.uwb.apex" />
-    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
-        <option name="cleanup-apks" value="true" />
-        <option name="test-file-name" value="CtsUwbTestCases.apk" />
-    </target_preparer>
-    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
-        <option name="package" value="android.uwb.cts" />
-    </test>
-    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
-        <option name="mainline-module-package-name" value="com.google.android.uwb" />
-    </object>
-</configuration>
diff --git a/tests/uwb/OWNERS b/tests/uwb/OWNERS
deleted file mode 100644
index c4ad416..0000000
--- a/tests/uwb/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# Bug component: 1042770
-include platform/packages/modules/Uwb:/OWNERS
diff --git a/tests/uwb/src/android/uwb/cts/AngleMeasurementTest.java b/tests/uwb/src/android/uwb/cts/AngleMeasurementTest.java
deleted file mode 100644
index f96b798..0000000
--- a/tests/uwb/src/android/uwb/cts/AngleMeasurementTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright 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 android.uwb.cts;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import android.os.Parcel;
-import android.uwb.AngleMeasurement;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test of {@link AngleMeasurement}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AngleMeasurementTest {
-    @Test
-    public void testConstructs() {
-        double radians = 0.1234;
-        double errorRadians = 0.5678;
-        double confidence = 0.5;
-
-        AngleMeasurement measurement = new AngleMeasurement(radians, errorRadians, confidence);
-        assertEquals(measurement.getRadians(), radians, 0);
-        assertEquals(measurement.getErrorRadians(), errorRadians, 0);
-        assertEquals(measurement.getConfidenceLevel(), confidence, 0);
-    }
-
-    @Test
-    public void testInvalidRadians() {
-        double radians = Math.PI + 0.01;
-        double errorRadians = 0.5678;
-        double confidence = 0.5;
-
-        constructExpectFailure(radians, errorRadians, confidence);
-        constructExpectFailure(-radians, errorRadians, confidence);
-    }
-
-    @Test
-    public void testInvalidErrorRadians() {
-        double radians = 0.1234;
-        double confidence = 0.5;
-
-        constructExpectFailure(radians, -0.01, confidence);
-        constructExpectFailure(-radians, Math.PI + 0.01, confidence);
-    }
-
-    @Test
-    public void testInvalidConfidence() {
-        double radians = 0.1234;
-        double errorRadians = 0.5678;
-
-        constructExpectFailure(radians, errorRadians, -0.01);
-        constructExpectFailure(radians, errorRadians, 1.01);
-    }
-
-    private void constructExpectFailure(double radians, double errorRadians, double confidence) {
-        try {
-            new AngleMeasurement(radians, errorRadians, confidence);
-            fail();
-        } catch (IllegalArgumentException e) {
-            // Expected
-        }
-    }
-
-    @Test
-    public void testParcel() {
-        Parcel parcel = Parcel.obtain();
-        AngleMeasurement measurement = UwbTestUtils.getAngleMeasurement();
-        measurement.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        AngleMeasurement fromParcel = AngleMeasurement.CREATOR.createFromParcel(parcel);
-        assertEquals(measurement, fromParcel);
-    }
-}
diff --git a/tests/uwb/src/android/uwb/cts/AngleOfArrivalMeasurementTest.java b/tests/uwb/src/android/uwb/cts/AngleOfArrivalMeasurementTest.java
deleted file mode 100644
index 085ce2e..0000000
--- a/tests/uwb/src/android/uwb/cts/AngleOfArrivalMeasurementTest.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 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 android.uwb.cts;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import android.os.Parcel;
-import android.uwb.AngleMeasurement;
-import android.uwb.AngleOfArrivalMeasurement;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test of {@link AngleOfArrivalMeasurement}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AngleOfArrivalMeasurementTest {
-
-    @Test
-    public void testBuilder() {
-        AngleMeasurement azimuth = UwbTestUtils.getAngleMeasurement();
-        AngleMeasurement altitude = UwbTestUtils.getAngleMeasurement();
-
-        AngleOfArrivalMeasurement.Builder builder = new AngleOfArrivalMeasurement.Builder(azimuth);
-        builder.setAltitude(altitude);
-
-        AngleOfArrivalMeasurement measurement = tryBuild(builder, true);
-
-        assertEquals(azimuth, measurement.getAzimuth());
-        assertEquals(altitude, measurement.getAltitude());
-    }
-
-    private AngleOfArrivalMeasurement tryBuild(AngleOfArrivalMeasurement.Builder builder,
-            boolean expectSuccess) {
-        AngleOfArrivalMeasurement measurement = null;
-        try {
-            measurement = builder.build();
-            if (!expectSuccess) {
-                fail("Expected AngleOfArrivalMeasurement.Builder.build() to fail");
-            }
-        } catch (IllegalStateException e) {
-            if (expectSuccess) {
-                fail("Expected AngleOfArrivalMeasurement.Builder.build() to succeed");
-            }
-        }
-        return measurement;
-    }
-
-    @Test
-    public void testParcel() {
-        Parcel parcel = Parcel.obtain();
-        AngleOfArrivalMeasurement measurement = UwbTestUtils.getAngleOfArrivalMeasurement();
-        measurement.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        AngleOfArrivalMeasurement fromParcel =
-                AngleOfArrivalMeasurement.CREATOR.createFromParcel(parcel);
-        assertEquals(measurement, fromParcel);
-    }
-}
diff --git a/tests/uwb/src/android/uwb/cts/DistanceMeasurementTest.java b/tests/uwb/src/android/uwb/cts/DistanceMeasurementTest.java
deleted file mode 100644
index fdebc78..0000000
--- a/tests/uwb/src/android/uwb/cts/DistanceMeasurementTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright 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 android.uwb.cts;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import android.os.Parcel;
-import android.uwb.DistanceMeasurement;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test of {@link DistanceMeasurement}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class DistanceMeasurementTest {
-    private static final double EPSILON = 0.00000000001;
-
-    @Test
-    public void testBuilder() {
-        double meters = 0.12;
-        double error = 0.54;
-        double confidence = 0.99;
-
-        DistanceMeasurement.Builder builder = new DistanceMeasurement.Builder();
-        tryBuild(builder, false);
-
-        builder.setMeters(meters);
-        tryBuild(builder, false);
-
-        builder.setErrorMeters(error);
-        tryBuild(builder, false);
-
-        builder.setConfidenceLevel(confidence);
-        DistanceMeasurement measurement = tryBuild(builder, true);
-
-        assertEquals(meters, measurement.getMeters(), 0);
-        assertEquals(error, measurement.getErrorMeters(), 0);
-        assertEquals(confidence, measurement.getConfidenceLevel(), 0);
-    }
-
-    private DistanceMeasurement tryBuild(DistanceMeasurement.Builder builder,
-            boolean expectSuccess) {
-        DistanceMeasurement measurement = null;
-        try {
-            measurement = builder.build();
-            if (!expectSuccess) {
-                fail("Expected DistanceMeasurement.Builder.build() to fail");
-            }
-        } catch (IllegalStateException e) {
-            if (expectSuccess) {
-                fail("Expected DistanceMeasurement.Builder.build() to succeed");
-            }
-        }
-        return measurement;
-    }
-
-    @Test
-    public void testParcel() {
-        Parcel parcel = Parcel.obtain();
-        DistanceMeasurement measurement = UwbTestUtils.getDistanceMeasurement();
-        measurement.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        DistanceMeasurement fromParcel =
-                DistanceMeasurement.CREATOR.createFromParcel(parcel);
-        assertEquals(measurement, fromParcel);
-    }
-}
diff --git a/tests/uwb/src/android/uwb/cts/RangingMeasurementTest.java b/tests/uwb/src/android/uwb/cts/RangingMeasurementTest.java
deleted file mode 100644
index bc5c2fd..0000000
--- a/tests/uwb/src/android/uwb/cts/RangingMeasurementTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright 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 android.uwb.cts;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.os.Parcel;
-import android.os.SystemClock;
-import android.uwb.AngleOfArrivalMeasurement;
-import android.uwb.DistanceMeasurement;
-import android.uwb.RangingMeasurement;
-import android.uwb.UwbAddress;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test of {@link RangingMeasurement}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class RangingMeasurementTest {
-    private static final int TEST_RSSI_DBM = -80;
-    private static final int INVALID_RSSI_DBM = -129;
-
-    @Test
-    public void testBuilder() {
-        int status = RangingMeasurement.RANGING_STATUS_SUCCESS;
-        UwbAddress address = UwbTestUtils.getUwbAddress(false);
-        long time = SystemClock.elapsedRealtimeNanos();
-        AngleOfArrivalMeasurement angleMeasurement = UwbTestUtils.getAngleOfArrivalMeasurement();
-        AngleOfArrivalMeasurement destinationAngleMeasurement =
-                UwbTestUtils.getAngleOfArrivalMeasurement();
-        DistanceMeasurement distanceMeasurement = UwbTestUtils.getDistanceMeasurement();
-        int los = RangingMeasurement.NLOS;
-        int measurementFocus = RangingMeasurement.MEASUREMENT_FOCUS_RANGE;
-
-
-        RangingMeasurement.Builder builder = new RangingMeasurement.Builder();
-
-        builder.setStatus(status);
-        tryBuild(builder, false);
-
-        builder.setElapsedRealtimeNanos(time);
-        tryBuild(builder, false);
-
-        builder.setAngleOfArrivalMeasurement(angleMeasurement);
-        tryBuild(builder, false);
-
-        builder.setDestinationAngleOfArrivalMeasurement(destinationAngleMeasurement);
-        tryBuild(builder, false);
-
-        builder.setDistanceMeasurement(distanceMeasurement);
-        tryBuild(builder, false);
-
-        builder.setRssiDbm(TEST_RSSI_DBM);
-        tryBuild(builder, false);
-
-        builder.setRemoteDeviceAddress(address);
-        tryBuild(builder, true);
-
-        builder.setLineOfSight(los);
-        tryBuild(builder, true);
-
-        builder.setMeasurementFocus(measurementFocus);
-        RangingMeasurement measurement = tryBuild(builder, true);
-
-        assertEquals(status, measurement.getStatus());
-        assertEquals(address, measurement.getRemoteDeviceAddress());
-        assertEquals(time, measurement.getElapsedRealtimeNanos());
-        assertEquals(angleMeasurement, measurement.getAngleOfArrivalMeasurement());
-        assertEquals(destinationAngleMeasurement,
-                measurement.getDestinationAngleOfArrivalMeasurement());
-        assertEquals(distanceMeasurement, measurement.getDistanceMeasurement());
-        assertEquals(los, measurement.getLineOfSight());
-        assertEquals(measurementFocus, measurement.getMeasurementFocus());
-        assertEquals(TEST_RSSI_DBM, measurement.getRssiDbm());
-    }
-
-    @Test
-    public void testInvalidRssi() {
-        RangingMeasurement.Builder builder = new RangingMeasurement.Builder();
-        try {
-            builder.setRssiDbm(INVALID_RSSI_DBM);
-            fail("Expected RangingMeasurement.Builder.setRssiDbm() to fail");
-        } catch (Exception e) {
-            assertTrue(e.getMessage().contains("Invalid"));
-        }
-    }
-
-    private RangingMeasurement tryBuild(RangingMeasurement.Builder builder,
-            boolean expectSuccess) {
-        RangingMeasurement measurement = null;
-        try {
-            measurement = builder.build();
-            if (!expectSuccess) {
-                fail("Expected RangingMeasurement.Builder.build() to fail");
-            }
-        } catch (IllegalStateException e) {
-            if (expectSuccess) {
-                fail("Expected DistanceMeasurement.Builder.build() to succeed");
-            }
-        }
-        return measurement;
-    }
-
-    @Test
-    public void testParcel() {
-        Parcel parcel = Parcel.obtain();
-        RangingMeasurement measurement = UwbTestUtils.getRangingMeasurement();
-        measurement.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        RangingMeasurement fromParcel = RangingMeasurement.CREATOR.createFromParcel(parcel);
-        assertEquals(measurement, fromParcel);
-    }
-}
diff --git a/tests/uwb/src/android/uwb/cts/RangingReportTest.java b/tests/uwb/src/android/uwb/cts/RangingReportTest.java
deleted file mode 100644
index b2524e7..0000000
--- a/tests/uwb/src/android/uwb/cts/RangingReportTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 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 android.uwb.cts;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-import android.os.Parcel;
-import android.uwb.RangingMeasurement;
-import android.uwb.RangingReport;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-
-/**
- * Test of {@link RangingReport}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class RangingReportTest {
-
-    @Test
-    public void testBuilder() {
-        List<RangingMeasurement> measurements = UwbTestUtils.getRangingMeasurements(5);
-
-        RangingReport.Builder builder = new RangingReport.Builder();
-        builder.addMeasurements(measurements);
-        RangingReport report = tryBuild(builder, true);
-        verifyMeasurementsEqual(measurements, report.getMeasurements());
-
-
-        builder = new RangingReport.Builder();
-        for (RangingMeasurement measurement : measurements) {
-            builder.addMeasurement(measurement);
-        }
-        report = tryBuild(builder, true);
-        verifyMeasurementsEqual(measurements, report.getMeasurements());
-    }
-
-    private void verifyMeasurementsEqual(List<RangingMeasurement> expected,
-            List<RangingMeasurement> actual) {
-        assertEquals(expected.size(), actual.size());
-        for (int i = 0; i < expected.size(); i++) {
-            assertEquals(expected.get(i), actual.get(i));
-        }
-    }
-
-    private RangingReport tryBuild(RangingReport.Builder builder,
-            boolean expectSuccess) {
-        RangingReport report = null;
-        try {
-            report = builder.build();
-            if (!expectSuccess) {
-                fail("Expected RangingReport.Builder.build() to fail");
-            }
-        } catch (IllegalStateException e) {
-            if (expectSuccess) {
-                fail("Expected RangingReport.Builder.build() to succeed");
-            }
-        }
-        return report;
-    }
-
-    @Test
-    public void testParcel() {
-        Parcel parcel = Parcel.obtain();
-        RangingReport report = UwbTestUtils.getRangingReports(5);
-        report.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        RangingReport fromParcel = RangingReport.CREATOR.createFromParcel(parcel);
-        assertEquals(report, fromParcel);
-    }
-}
diff --git a/tests/uwb/src/android/uwb/cts/UwbAddressTest.java b/tests/uwb/src/android/uwb/cts/UwbAddressTest.java
deleted file mode 100644
index d2f4228..0000000
--- a/tests/uwb/src/android/uwb/cts/UwbAddressTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 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 android.uwb.cts;
-
-import static org.junit.Assert.assertEquals;
-
-import android.os.Parcel;
-import android.uwb.UwbAddress;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test of {@link UwbAddress}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class UwbAddressTest {
-
-    @Test
-    public void testFromBytes_Short() {
-        runFromBytes(UwbAddress.SHORT_ADDRESS_BYTE_LENGTH);
-    }
-
-    @Test
-    public void testFromBytes_Extended() {
-        runFromBytes(UwbAddress.EXTENDED_ADDRESS_BYTE_LENGTH);
-    }
-
-    private void runFromBytes(int len) {
-        byte[] addressBytes = getByteArray(len);
-        UwbAddress address = UwbAddress.fromBytes(addressBytes);
-        assertEquals(address.size(), len);
-        assertEquals(addressBytes, address.toBytes());
-    }
-
-    private byte[] getByteArray(int len) {
-        byte[] res = new byte[len];
-        for (int i = 0; i < len; i++) {
-            res[i] = (byte) i;
-        }
-        return res;
-    }
-
-    @Test
-    public void testParcel_Short() {
-        runParcel(true);
-    }
-
-    @Test
-    public void testParcel_Extended() {
-        runParcel(false);
-    }
-
-    private void runParcel(boolean useShortAddress) {
-        Parcel parcel = Parcel.obtain();
-        UwbAddress address = UwbTestUtils.getUwbAddress(useShortAddress);
-        address.writeToParcel(parcel, 0);
-        parcel.setDataPosition(0);
-        UwbAddress fromParcel = UwbAddress.CREATOR.createFromParcel(parcel);
-        assertEquals(address, fromParcel);
-    }
-}
diff --git a/tests/uwb/src/android/uwb/cts/UwbFrameworkInitializerTest.java b/tests/uwb/src/android/uwb/cts/UwbFrameworkInitializerTest.java
deleted file mode 100644
index ddd22c6..0000000
--- a/tests/uwb/src/android/uwb/cts/UwbFrameworkInitializerTest.java
+++ /dev/null
@@ -1,68 +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 android.uwb.cts;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Context;
-import android.platform.test.annotations.AppModeFull;
-import android.uwb.UwbFrameworkInitializer;
-import android.uwb.UwbManager;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-/**
- * Test of {@link UwbManager}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Cannot get UwbManager in instant app mode")
-public class UwbFrameworkInitializerTest {
-    private final Context mContext = InstrumentationRegistry.getContext();
-    private UwbManager mUwbManager;
-
-    @Before
-    public void setup() throws Exception {
-        mUwbManager = mContext.getSystemService(UwbManager.class);
-        assumeTrue(UwbTestUtils.isUwbSupported(mContext));
-        assertThat(mUwbManager).isNotNull();
-    }
-
-    /**
-     * UwbFrameworkInitializer.registerServiceWrappers() should only be called by
-     * SystemServiceRegistry during boot up when Uwb is first initialized. Calling this API at
-     * any other time should throw an exception.
-     */
-    @Test
-    public void testRegisterServiceWrappers_failsWhenCalledOutsideOfSystemServiceRegistry() {
-        try {
-            UwbFrameworkInitializer.registerServiceWrappers();
-            fail("Expected exception when calling "
-                    + "UwbFrameworkInitializer.registerServiceWrappers() outside of "
-                    + "SystemServiceRegistry!");
-        } catch (IllegalStateException expected) { }
-    }
-}
diff --git a/tests/uwb/src/android/uwb/cts/UwbManagerTest.java b/tests/uwb/src/android/uwb/cts/UwbManagerTest.java
deleted file mode 100644
index e9c5008..0000000
--- a/tests/uwb/src/android/uwb/cts/UwbManagerTest.java
+++ /dev/null
@@ -1,1017 +0,0 @@
-/*
- * Copyright 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 android.uwb.cts;
-
-import static android.Manifest.permission.UWB_PRIVILEGED;
-import static android.Manifest.permission.UWB_RANGING;
-import static android.uwb.UwbManager.AdapterStateCallback.STATE_DISABLED;
-import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_ACTIVE;
-import static android.uwb.UwbManager.AdapterStateCallback.STATE_ENABLED_INACTIVE;
-
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.UiAutomation;
-import android.content.AttributionSource;
-import android.content.Context;
-import android.content.ContextParams;
-import android.os.CancellationSignal;
-import android.os.PersistableBundle;
-import android.os.Process;
-import android.permission.PermissionManager;
-import android.platform.test.annotations.AppModeFull;
-import android.util.Log;
-import android.uwb.RangingReport;
-import android.uwb.RangingSession;
-import android.uwb.UwbAddress;
-import android.uwb.UwbManager;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.compatibility.common.util.CddTest;
-import com.android.compatibility.common.util.ShellIdentityUtils;
-
-import com.google.uwb.support.fira.FiraOpenSessionParams;
-import com.google.uwb.support.fira.FiraParams;
-import com.google.uwb.support.fira.FiraProtocolVersion;
-import com.google.uwb.support.multichip.ChipInfoParams;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-import java.util.Random;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Test of {@link UwbManager}.
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@AppModeFull(reason = "Cannot get UwbManager in instant app mode")
-public class UwbManagerTest {
-    private static final String TAG = "UwbManagerTest";
-
-    private final Context mContext = InstrumentationRegistry.getContext();
-    private UwbManager mUwbManager;
-    private String mDefaultChipId;
-
-    @Before
-    public void setup() throws Exception {
-        mUwbManager = mContext.getSystemService(UwbManager.class);
-        assumeTrue(UwbTestUtils.isUwbSupported(mContext));
-        assertThat(mUwbManager).isNotNull();
-
-        // Ensure UWB is toggled on.
-        ShellIdentityUtils.invokeWithShellPermissions(() -> {
-            if (!mUwbManager.isUwbEnabled()) {
-                try {
-                    setUwbEnabledAndWaitForCompletion(true);
-                } catch (Exception e) {
-                    fail("Exception while processing UWB toggle " + e);
-                }
-            }
-            mDefaultChipId = mUwbManager.getDefaultChipId();
-        });
-    }
-
-    // Should be invoked with shell permissions.
-    private void setUwbEnabledAndWaitForCompletion(boolean enabled) throws Exception {
-        CountDownLatch countDownLatch = new CountDownLatch(1);
-        int adapterState = enabled ? STATE_ENABLED_INACTIVE : STATE_DISABLED;
-        AdapterStateCallback adapterStateCallback =
-                new AdapterStateCallback(countDownLatch, adapterState);
-        try {
-            mUwbManager.registerAdapterStateCallback(
-                    Executors.newSingleThreadExecutor(), adapterStateCallback);
-            mUwbManager.setUwbEnabled(enabled);
-            assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
-            assertThat(mUwbManager.isUwbEnabled()).isEqualTo(enabled);
-            assertThat(adapterStateCallback.state).isEqualTo(adapterState);
-        } finally {
-            mUwbManager.unregisterAdapterStateCallback(adapterStateCallback);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetSpecificationInfo() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            PersistableBundle persistableBundle = mUwbManager.getSpecificationInfo();
-            assertThat(persistableBundle).isNotNull();
-            assertThat(persistableBundle.isEmpty()).isFalse();
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetSpecificationInfoWithChipId() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            PersistableBundle persistableBundle =
-                    mUwbManager.getSpecificationInfo(mDefaultChipId);
-            assertThat(persistableBundle).isNotNull();
-            assertThat(persistableBundle.isEmpty()).isFalse();
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetChipInfos() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            List<PersistableBundle> chipInfos = mUwbManager.getChipInfos();
-            assertThat(chipInfos).hasSize(1);
-            ChipInfoParams chipInfoParams = ChipInfoParams.fromBundle(chipInfos.get(0));
-            assertThat(chipInfoParams.getChipId()).isEqualTo(mDefaultChipId);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetSpecificationInfoWithInvalidChipId() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            assertThrows(IllegalArgumentException.class,
-                    () -> mUwbManager.getSpecificationInfo("invalidChipId"));
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetSpecificationInfoWithoutUwbPrivileged() {
-        try {
-            mUwbManager.getSpecificationInfo();
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetSpecificationInfoWithChipIdWithoutUwbPrivileged() {
-        try {
-            mUwbManager.getSpecificationInfo(mDefaultChipId);
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testElapsedRealtimeResolutionNanos() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            assertThat(mUwbManager.elapsedRealtimeResolutionNanos() >= 0L).isTrue();
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testElapsedRealtimeResolutionNanosWithChipId() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            assertThat(mUwbManager.elapsedRealtimeResolutionNanos(mDefaultChipId) >= 0L)
-                    .isTrue();
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testElapsedRealtimeResolutionNanosWithInvalidChipId() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            assertThrows(IllegalArgumentException.class,
-                    () -> mUwbManager.elapsedRealtimeResolutionNanos("invalidChipId"));
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testElapsedRealtimeResolutionNanosWithoutUwbPrivileged() {
-        try {
-            mUwbManager.elapsedRealtimeResolutionNanos();
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testElapsedRealtimeResolutionNanosWithChipIdWithoutUwbPrivileged() {
-        try {
-            mUwbManager.elapsedRealtimeResolutionNanos(mDefaultChipId);
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testAddServiceProfileWithoutUwbPrivileged() {
-        try {
-            mUwbManager.addServiceProfile(new PersistableBundle());
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testRemoveServiceProfileWithoutUwbPrivileged() {
-        try {
-            mUwbManager.removeServiceProfile(new PersistableBundle());
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetAllServiceProfilesWithoutUwbPrivileged() {
-        try {
-            mUwbManager.getAllServiceProfiles();
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetAdfProvisioningAuthoritiesWithoutUwbPrivileged() {
-        try {
-            mUwbManager.getAdfProvisioningAuthorities(new PersistableBundle());
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetAdfCertificateInfoWithoutUwbPrivileged() {
-        try {
-            mUwbManager.getAdfCertificateInfo(new PersistableBundle());
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testGetChipInfosWithoutUwbPrivileged() {
-        try {
-            mUwbManager.getChipInfos();
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testSendVendorUciWithoutUwbPrivileged() {
-        try {
-            mUwbManager.sendVendorUciMessage(10, 0, new byte[0]);
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    private class AdfProvisionStateCallback extends UwbManager.AdfProvisionStateCallback {
-        private final CountDownLatch mCountDownLatch;
-
-        public boolean onSuccessCalled;
-        public boolean onFailedCalled;
-
-        AdfProvisionStateCallback(@NonNull CountDownLatch countDownLatch) {
-            mCountDownLatch = countDownLatch;
-        }
-
-        @Override
-        public void onProfileAdfsProvisioned(@NonNull PersistableBundle params) {
-            onSuccessCalled = true;
-            mCountDownLatch.countDown();
-        }
-
-        @Override
-        public void onProfileAdfsProvisionFailed(int reason, @NonNull PersistableBundle params) {
-            onFailedCalled = true;
-            mCountDownLatch.countDown();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testProvisionProfileAdfByScriptWithoutUwbPrivileged() {
-        CountDownLatch countDownLatch = new CountDownLatch(1);
-        AdfProvisionStateCallback adfProvisionStateCallback =
-                new AdfProvisionStateCallback(countDownLatch);
-        try {
-            mUwbManager.provisionProfileAdfByScript(
-                    new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    adfProvisionStateCallback);
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testRemoveProfileAdfWithoutUwbPrivileged() {
-        try {
-            mUwbManager.removeProfileAdf(new PersistableBundle());
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    private class UwbVendorUciCallback implements UwbManager.UwbVendorUciCallback {
-        private final CountDownLatch mRspCountDownLatch;
-        private final CountDownLatch mNtfCountDownLatch;
-
-        public int gid;
-        public int oid;
-        public byte[] payload;
-
-        UwbVendorUciCallback(
-                @NonNull CountDownLatch rspCountDownLatch,
-                @NonNull CountDownLatch ntfCountDownLatch) {
-            mRspCountDownLatch = rspCountDownLatch;
-            mNtfCountDownLatch = ntfCountDownLatch;
-        }
-
-        @Override
-        public void onVendorUciResponse(int gid, int oid, byte[] payload) {
-            this.gid = gid;
-            this.oid = oid;
-            this.payload = payload;
-            mRspCountDownLatch.countDown();
-        }
-
-        @Override
-        public void onVendorUciNotification(int gid, int oid, byte[] payload) {
-            this.gid = gid;
-            this.oid = oid;
-            this.payload = payload;
-            mNtfCountDownLatch.countDown();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testRegisterVendorUciCallbackWithoutUwbPrivileged() {
-        UwbManager.UwbVendorUciCallback cb =
-                new UwbVendorUciCallback(new CountDownLatch(1), new CountDownLatch(1));
-        try {
-            mUwbManager.registerUwbVendorUciCallback(
-                    Executors.newSingleThreadExecutor(), cb);
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testUnregisterVendorUciCallbackWithoutUwbPrivileged() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        UwbManager.UwbVendorUciCallback cb =
-                new UwbVendorUciCallback(new CountDownLatch(1), new CountDownLatch(1));
-        try {
-            // Needs UWB_PRIVILEGED & UWB_RANGING permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            mUwbManager.registerUwbVendorUciCallback(
-                    Executors.newSingleThreadExecutor(), cb);
-        } catch (SecurityException e) {
-            /* pass */
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-        try {
-            mUwbManager.unregisterUwbVendorUciCallback(cb);
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testInvalidCallbackUnregisterVendorUciCallback() {
-        UwbManager.UwbVendorUciCallback cb =
-                new UwbVendorUciCallback(new CountDownLatch(1), new CountDownLatch(1));
-        try {
-            mUwbManager.registerUwbVendorUciCallback(
-                    Executors.newSingleThreadExecutor(), cb);
-        } catch (SecurityException e) {
-            /* registration failed */
-        }
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            mUwbManager.unregisterUwbVendorUciCallback(cb);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    private class RangingSessionCallback implements RangingSession.Callback {
-        private CountDownLatch mCtrlCountDownLatch;
-        private CountDownLatch mResultCountDownLatch;
-
-        public boolean onOpenedCalled;
-        public boolean onOpenFailedCalled;
-        public boolean onStartedCalled;
-        public boolean onStartFailedCalled;
-        public boolean onClosedCalled;
-        public RangingSession rangingSession;
-        public RangingReport rangingReport;
-
-        RangingSessionCallback(
-                @NonNull CountDownLatch ctrlCountDownLatch) {
-            this(ctrlCountDownLatch, null /* resultCountDownLaynch */);
-        }
-
-        RangingSessionCallback(
-                @NonNull CountDownLatch ctrlCountDownLatch,
-                @Nullable CountDownLatch resultCountDownLatch) {
-            mCtrlCountDownLatch = ctrlCountDownLatch;
-            mResultCountDownLatch = resultCountDownLatch;
-        }
-
-        public void replaceCtrlCountDownLatch(@NonNull CountDownLatch countDownLatch) {
-            mCtrlCountDownLatch = countDownLatch;
-        }
-
-        public void onOpened(@NonNull RangingSession session) {
-            onOpenedCalled = true;
-            rangingSession = session;
-            mCtrlCountDownLatch.countDown();
-        }
-
-        public void onOpenFailed(@Reason int reason, @NonNull PersistableBundle params) {
-            onOpenFailedCalled = true;
-            mCtrlCountDownLatch.countDown();
-        }
-
-        public void onStarted(@NonNull PersistableBundle sessionInfo) {
-            onStartedCalled = true;
-            mCtrlCountDownLatch.countDown();
-        }
-
-        public void onStartFailed(@Reason int reason, @NonNull PersistableBundle params) {
-            onStartFailedCalled = true;
-            mCtrlCountDownLatch.countDown();
-        }
-
-        public void onReconfigured(@NonNull PersistableBundle params) { }
-
-        public void onReconfigureFailed(@Reason int reason, @NonNull PersistableBundle params) { }
-
-        public void onStopped(@Reason int reason, @NonNull PersistableBundle parameters) { }
-
-        public void onStopFailed(@Reason int reason, @NonNull PersistableBundle params) { }
-
-        public void onClosed(@Reason int reason, @NonNull PersistableBundle parameters) {
-            onClosedCalled = true;
-            mCtrlCountDownLatch.countDown();
-        }
-
-        public void onReportReceived(@NonNull RangingReport rangingReport) {
-            if (mResultCountDownLatch != null) {
-                this.rangingReport = rangingReport;
-                mResultCountDownLatch.countDown();
-            }
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testOpenRangingSessionWithInvalidChipId() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        CountDownLatch countDownLatch = new CountDownLatch(1);
-        RangingSessionCallback rangingSessionCallback = new RangingSessionCallback(countDownLatch);
-        try {
-            // Needs UWB_PRIVILEGED & UWB_RANGING permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            // Try to start a ranging session with invalid params, should fail.
-            assertThrows(IllegalArgumentException.class, () -> mUwbManager.openRangingSession(
-                    new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    rangingSessionCallback,
-                    "invalidChipId"));
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testOpenRangingSessionWithChipIdWithBadParams() throws Exception {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        CancellationSignal cancellationSignal = null;
-        CountDownLatch countDownLatch = new CountDownLatch(1);
-        RangingSessionCallback rangingSessionCallback = new RangingSessionCallback(countDownLatch);
-        try {
-            // Needs UWB_PRIVILEGED & UWB_RANGING permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            // Try to start a ranging session with invalid params, should fail.
-            cancellationSignal = mUwbManager.openRangingSession(
-                    new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    rangingSessionCallback,
-                    mDefaultChipId);
-            // Wait for the on start failed callback.
-            assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
-            assertThat(rangingSessionCallback.onOpenedCalled).isFalse();
-            assertThat(rangingSessionCallback.onOpenFailedCalled).isTrue();
-        } finally {
-            if (cancellationSignal != null) {
-                cancellationSignal.cancel();
-            }
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testOpenRangingSessionWithInvalidChipIdWithBadParams() throws Exception {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        CancellationSignal cancellationSignal = null;
-        CountDownLatch countDownLatch = new CountDownLatch(1);
-        RangingSessionCallback rangingSessionCallback = new RangingSessionCallback(countDownLatch);
-        try {
-            // Needs UWB_PRIVILEGED & UWB_RANGING permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            // Try to start a ranging session with invalid params, should fail.
-            cancellationSignal = mUwbManager.openRangingSession(
-                    new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    rangingSessionCallback,
-                    mDefaultChipId);
-            // Wait for the on start failed callback.
-            assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
-            assertThat(rangingSessionCallback.onOpenedCalled).isFalse();
-            assertThat(rangingSessionCallback.onOpenFailedCalled).isTrue();
-        } finally {
-            if (cancellationSignal != null) {
-                cancellationSignal.cancel();
-            }
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    /**
-     * Simulates the app holding UWB_RANGING permission, but not UWB_PRIVILEGED.
-     */
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testOpenRangingSessionWithoutUwbPrivileged() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Only hold UWB_RANGING permission
-            uiAutomation.adoptShellPermissionIdentity(UWB_RANGING);
-            mUwbManager.openRangingSession(new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    new RangingSessionCallback(new CountDownLatch(1)));
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testOpenRangingSessionWithChipIdWithoutUwbPrivileged() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Only hold UWB_RANGING permission
-            uiAutomation.adoptShellPermissionIdentity(UWB_RANGING);
-            mUwbManager.openRangingSession(new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    new RangingSessionCallback(new CountDownLatch(1)),
-                    mDefaultChipId);
-            // should fail if the call was successful without UWB_PRIVILEGED permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    /**
-     * Simulates the app holding UWB_PRIVILEGED permission, but not UWB_RANGING.
-     */
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testOpenRangingSessionWithoutUwbRanging() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity(UWB_PRIVILEGED);
-            mUwbManager.openRangingSession(new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    new RangingSessionCallback(new CountDownLatch(1)));
-            // should fail if the call was successful without UWB_RANGING permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testOpenRangingSessionWithChipIdWithoutUwbRanging() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity(UWB_PRIVILEGED);
-            mUwbManager.openRangingSession(new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    new RangingSessionCallback(new CountDownLatch(1)),
-                    mDefaultChipId);
-            // should fail if the call was successful without UWB_RANGING permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    private AttributionSource getShellAttributionSourceWithRenouncedPermissions(
-            @Nullable Set<String> renouncedPermissions) {
-        try {
-            AttributionSource shellAttributionSource =
-                    new AttributionSource.Builder(Process.SHELL_UID)
-                            .setPackageName("com.android.shell")
-                            .setRenouncedPermissions(renouncedPermissions)
-                            .build();
-            PermissionManager permissionManager =
-                    mContext.getSystemService(PermissionManager.class);
-            permissionManager.registerAttributionSource(shellAttributionSource);
-            return shellAttributionSource;
-        } catch (SecurityException e) {
-            fail("Failed to create shell attribution source" + e);
-            return null;
-        }
-    }
-
-    private Context createShellContextWithRenouncedPermissionsAndAttributionSource(
-            @Nullable Set<String> renouncedPermissions) {
-        return mContext.createContext(new ContextParams.Builder()
-                .setRenouncedPermissions(renouncedPermissions)
-                .setNextAttributionSource(
-                        getShellAttributionSourceWithRenouncedPermissions(renouncedPermissions))
-                .build());
-    }
-
-    /**
-     * Simulates the calling app holding UWB_PRIVILEGED permission and UWB_RANGING permission, but
-     * the proxied app not holding UWB_RANGING permission.
-     */
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2,C-1-5"})
-    public void testOpenRangingSessionWithoutUwbRangingInNextAttributeSource() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Only hold UWB_PRIVILEGED permission
-            uiAutomation.adoptShellPermissionIdentity();
-            Context shellContextWithUwbRangingRenounced =
-                    createShellContextWithRenouncedPermissionsAndAttributionSource(
-                            Set.of(UWB_RANGING));
-            UwbManager uwbManagerWithUwbRangingRenounced =
-                    shellContextWithUwbRangingRenounced.getSystemService(UwbManager.class);
-            uwbManagerWithUwbRangingRenounced.openRangingSession(new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    new RangingSessionCallback(new CountDownLatch(1)));
-            // should fail if the call was successful without UWB_RANGING permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2,C-1-5"})
-    public void testOpenRangingSessionWithChipIdWithoutUwbRangingInNextAttributeSource() {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Only hold UWB_PRIVILEGED permission
-            uiAutomation.adoptShellPermissionIdentity();
-            Context shellContextWithUwbRangingRenounced =
-                    createShellContextWithRenouncedPermissionsAndAttributionSource(
-                            Set.of(UWB_RANGING));
-            UwbManager uwbManagerWithUwbRangingRenounced =
-                    shellContextWithUwbRangingRenounced.getSystemService(UwbManager.class);
-            uwbManagerWithUwbRangingRenounced.openRangingSession(new PersistableBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    new RangingSessionCallback(new CountDownLatch(1)),
-                    mDefaultChipId);
-            // should fail if the call was successful without UWB_RANGING permission.
-            fail();
-        } catch (SecurityException e) {
-            /* pass */
-            Log.i(TAG, "Failed with expected security exception: " + e);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2,C-1-5"})
-    public void testFiraRangingSession() throws Exception {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        CancellationSignal cancellationSignal = null;
-        CountDownLatch countDownLatch = new CountDownLatch(1);
-        CountDownLatch resultCountDownLatch = new CountDownLatch(1);
-        RangingSessionCallback rangingSessionCallback =
-                new RangingSessionCallback(countDownLatch, resultCountDownLatch);
-        FiraOpenSessionParams firaOpenSessionParams = new FiraOpenSessionParams.Builder()
-                .setProtocolVersion(new FiraProtocolVersion(1, 1))
-                .setSessionId(1)
-                .setStsConfig(FiraParams.STS_CONFIG_STATIC)
-                .setVendorId(new byte[]{0x5, 0x6})
-                .setStaticStsIV(new byte[]{0x5, 0x6, 0x9, 0xa, 0x4, 0x6})
-                .setDeviceType(FiraParams.RANGING_DEVICE_TYPE_CONTROLLER)
-                .setDeviceRole(FiraParams.RANGING_DEVICE_ROLE_INITIATOR)
-                .setMultiNodeMode(FiraParams.MULTI_NODE_MODE_UNICAST)
-                .setDeviceAddress(UwbAddress.fromBytes(new byte[] {0x5, 6}))
-                .setDestAddressList(List.of(UwbAddress.fromBytes(new byte[] {0x5, 6})))
-                .setSlotDurationRstu(2400)
-                .setSlotsPerRangingRound(25)
-                .setRangingIntervalMs(200)
-                .build();
-        try {
-            // Needs UWB_PRIVILEGED & UWB_RANGING permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            // Try to start a ranging session with invalid params, should fail.
-            cancellationSignal = mUwbManager.openRangingSession(
-                    firaOpenSessionParams.toBundle(),
-                    Executors.newSingleThreadExecutor(),
-                    rangingSessionCallback,
-                    mDefaultChipId);
-            // Wait for the on opened callback.
-            assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
-            assertThat(rangingSessionCallback.onOpenedCalled).isTrue();
-            assertThat(rangingSessionCallback.onOpenFailedCalled).isFalse();
-            assertThat(rangingSessionCallback.rangingSession).isNotNull();
-
-            countDownLatch = new CountDownLatch(1);
-            rangingSessionCallback.replaceCtrlCountDownLatch(countDownLatch);
-            rangingSessionCallback.rangingSession.start(new PersistableBundle());
-            // Wait for the on started callback.
-            assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
-            assertThat(rangingSessionCallback.onStartedCalled).isTrue();
-            assertThat(rangingSessionCallback.onStartFailedCalled).isFalse();
-
-            // Wait for the on ranging report callback.
-            assertThat(resultCountDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
-            assertThat(rangingSessionCallback.rangingReport).isNotNull();
-
-            // Check the UWB state.
-            assertThat(mUwbManager.getAdapterState()).isEqualTo(STATE_ENABLED_ACTIVE);
-
-            // Stop ongoing session.
-            rangingSessionCallback.rangingSession.stop();
-        } finally {
-            if (cancellationSignal != null) {
-                countDownLatch = new CountDownLatch(1);
-                rangingSessionCallback.replaceCtrlCountDownLatch(countDownLatch);
-
-                // Close session.
-                cancellationSignal.cancel();
-
-                // Wait for the on closed callback.
-                assertThat(countDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
-                assertThat(rangingSessionCallback.onClosedCalled).isTrue();
-            }
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    private class AdapterStateCallback implements UwbManager.AdapterStateCallback {
-        private final CountDownLatch mCountDownLatch;
-        private final @State Integer mWaitForState;
-        public int state;
-        public int reason;
-
-        AdapterStateCallback(@NonNull CountDownLatch countDownLatch,
-                @Nullable @State Integer waitForState) {
-            mCountDownLatch = countDownLatch;
-            mWaitForState = waitForState;
-        }
-
-        public void onStateChanged(@State int state, @StateChangedReason int reason) {
-            this.state = state;
-            this.reason = reason;
-            if (mWaitForState != null) {
-                if (mWaitForState == state) {
-                    mCountDownLatch.countDown();
-                }
-            } else {
-                mCountDownLatch.countDown();
-            }
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2,C-1-4"})
-    public void testUwbStateToggle() throws Exception {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        try {
-            // Needs UWB_PRIVILEGED permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            assertThat(mUwbManager.isUwbEnabled()).isTrue();
-            assertThat(mUwbManager.getAdapterState()).isEqualTo(STATE_ENABLED_INACTIVE);
-            // Toggle the state
-            setUwbEnabledAndWaitForCompletion(false);
-            assertThat(mUwbManager.getAdapterState()).isEqualTo(STATE_DISABLED);
-        } finally {
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testSendVendorUciMessage() throws Exception {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        CountDownLatch rspCountDownLatch = new CountDownLatch(1);
-        CountDownLatch ntfCountDownLatch = new CountDownLatch(1);
-        UwbVendorUciCallback cb =
-                new UwbVendorUciCallback(rspCountDownLatch, ntfCountDownLatch);
-        try {
-            // Needs UWB_PRIVILEGED & UWB_RANGING permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            mUwbManager.registerUwbVendorUciCallback(
-                    Executors.newSingleThreadExecutor(), cb);
-
-            // Send random payload with a vendor gid.
-            byte[] payload = new byte[100];
-            new Random().nextBytes(payload);
-            int gid = 9;
-            int oid = 1;
-            mUwbManager.sendVendorUciMessage(gid, oid, payload);
-
-            // Wait for response.
-            assertThat(rspCountDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
-            assertThat(cb.gid).isEqualTo(gid);
-            assertThat(cb.oid).isEqualTo(oid);
-            assertThat(cb.payload).isNotEmpty();
-        } catch (SecurityException e) {
-            /* pass */
-        } finally {
-            mUwbManager.unregisterUwbVendorUciCallback(cb);
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-
-    @Test
-    @CddTest(requirements = {"7.3.13/C-1-1,C-1-2"})
-    public void testSendVendorUciMessageWithFragmentedPackets() throws Exception {
-        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
-        CountDownLatch rspCountDownLatch = new CountDownLatch(1);
-        CountDownLatch ntfCountDownLatch = new CountDownLatch(1);
-        UwbVendorUciCallback cb =
-                new UwbVendorUciCallback(rspCountDownLatch, ntfCountDownLatch);
-        try {
-            // Needs UWB_PRIVILEGED & UWB_RANGING permission which is held by shell.
-            uiAutomation.adoptShellPermissionIdentity();
-            mUwbManager.registerUwbVendorUciCallback(
-                    Executors.newSingleThreadExecutor(), cb);
-
-            // Send random payload > 255 bytes with a vendor gid.
-            byte[] payload = new byte[400];
-            new Random().nextBytes(payload);
-            int gid = 9;
-            int oid = 1;
-            mUwbManager.sendVendorUciMessage(gid, oid, payload);
-
-            // Wait for response.
-            assertThat(rspCountDownLatch.await(1, TimeUnit.SECONDS)).isTrue();
-            assertThat(cb.gid).isEqualTo(gid);
-            assertThat(cb.oid).isEqualTo(oid);
-            assertThat(cb.payload).isNotEmpty();
-        } catch (SecurityException e) {
-            /* pass */
-        } finally {
-            mUwbManager.unregisterUwbVendorUciCallback(cb);
-            uiAutomation.dropShellPermissionIdentity();
-        }
-    }
-}
diff --git a/tests/uwb/src/android/uwb/cts/UwbTestUtils.java b/tests/uwb/src/android/uwb/cts/UwbTestUtils.java
deleted file mode 100644
index 041b66c..0000000
--- a/tests/uwb/src/android/uwb/cts/UwbTestUtils.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright 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 android.uwb.cts;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.SystemClock;
-import android.uwb.AngleMeasurement;
-import android.uwb.AngleOfArrivalMeasurement;
-import android.uwb.DistanceMeasurement;
-import android.uwb.RangingMeasurement;
-import android.uwb.RangingReport;
-import android.uwb.UwbAddress;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-public class UwbTestUtils {
-    private UwbTestUtils() {}
-
-    public static boolean isUwbSupported(Context context) {
-        PackageManager packageManager = context.getPackageManager();
-        return packageManager.hasSystemFeature(PackageManager.FEATURE_UWB);
-    }
-
-    public static AngleMeasurement getAngleMeasurement() {
-        return new AngleMeasurement(
-                getDoubleInRange(-Math.PI, Math.PI),
-                getDoubleInRange(0, Math.PI),
-                getDoubleInRange(0, 1));
-    }
-
-    public static AngleOfArrivalMeasurement getAngleOfArrivalMeasurement() {
-        return new AngleOfArrivalMeasurement.Builder(getAngleMeasurement())
-                .setAltitude(getAngleMeasurement())
-                .build();
-    }
-
-    public static DistanceMeasurement getDistanceMeasurement() {
-        return new DistanceMeasurement.Builder()
-                .setMeters(getDoubleInRange(0, 100))
-                .setErrorMeters(getDoubleInRange(0, 10))
-                .setConfidenceLevel(getDoubleInRange(0, 1))
-                .build();
-    }
-
-    public static RangingMeasurement getRangingMeasurement() {
-        return getRangingMeasurement(getUwbAddress(false));
-    }
-
-    public static RangingMeasurement getRangingMeasurement(UwbAddress address) {
-        return new RangingMeasurement.Builder()
-                .setDistanceMeasurement(getDistanceMeasurement())
-                .setAngleOfArrivalMeasurement(getAngleOfArrivalMeasurement())
-                .setDestinationAngleOfArrivalMeasurement(getAngleOfArrivalMeasurement())
-                .setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos())
-                .setRemoteDeviceAddress(address != null ? address : getUwbAddress(false))
-                .setStatus(RangingMeasurement.RANGING_STATUS_SUCCESS)
-                .setLineOfSight(RangingMeasurement.NLOS)
-                .setMeasurementFocus(RangingMeasurement.MEASUREMENT_FOCUS_RANGE)
-                .setRssiDbm(-85)
-                .build();
-    }
-
-    public static List<RangingMeasurement> getRangingMeasurements(int num) {
-        List<RangingMeasurement> result = new ArrayList<>();
-        for (int i = 0; i < num; i++) {
-            result.add(getRangingMeasurement());
-        }
-        return result;
-    }
-
-    public static RangingReport getRangingReports(int numMeasurements) {
-        RangingReport.Builder builder = new RangingReport.Builder();
-        for (int i = 0; i < numMeasurements; i++) {
-            builder.addMeasurement(getRangingMeasurement());
-        }
-        return builder.build();
-    }
-
-    private static double getDoubleInRange(double min, double max) {
-        return min + (max - min) * Math.random();
-    }
-
-    public static UwbAddress getUwbAddress(boolean isShortAddress) {
-        byte[] addressBytes = new byte[isShortAddress ? UwbAddress.SHORT_ADDRESS_BYTE_LENGTH :
-                UwbAddress.EXTENDED_ADDRESS_BYTE_LENGTH];
-        for (int i = 0; i < addressBytes.length; i++) {
-            addressBytes[i] = (byte) getDoubleInRange(1, 255);
-        }
-        return UwbAddress.fromBytes(addressBytes);
-    }
-
-    public static Executor getExecutor() {
-        return new Executor() {
-            @Override
-            public void execute(Runnable command) {
-                command.run();
-            }
-        };
-    }
-}
diff --git a/tools/cts-device-info/Android.bp b/tools/cts-device-info/Android.bp
index 95487a6..3fac250 100644
--- a/tools/cts-device-info/Android.bp
+++ b/tools/cts-device-info/Android.bp
@@ -30,7 +30,7 @@
         "cts",
         "general-tests",
         "sts",
-        "mts",
+        "mts-mainline-infra",
         "vts",
         "catbox",
         "gcatbox",
diff --git a/tools/cts-media-preparer-app/Android.bp b/tools/cts-media-preparer-app/Android.bp
index cbad568..7fe2a66 100644
--- a/tools/cts-media-preparer-app/Android.bp
+++ b/tools/cts-media-preparer-app/Android.bp
@@ -34,7 +34,7 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts",
+        "mts-media",
     ],
     sdk_version: "test_current",
     min_sdk_version: "29",
diff --git a/tools/cts-tradefed/tests/Android.bp b/tools/cts-tradefed/tests/Android.bp
index 51de025..5e1789f 100644
--- a/tools/cts-tradefed/tests/Android.bp
+++ b/tools/cts-tradefed/tests/Android.bp
@@ -28,3 +28,15 @@
     // We ship the Deqp Runner tests with the CTS one to validate them.
     static_libs: ["CtsDeqpRunnerTests"],
 }
+
+// Provide a common loading test that can be reused in other suites
+java_library_host {
+    name: "suite-loading-tests",
+
+    srcs: ["src/com/android/compatibility/common/tradefed/loading/*.java"],
+
+    libs: [
+        "tradefed",
+        "cts-tradefed",
+    ],
+}
diff --git a/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/loading/CommonConfigLoadingTest.java b/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/loading/CommonConfigLoadingTest.java
new file mode 100644
index 0000000..36ee734
--- /dev/null
+++ b/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/loading/CommonConfigLoadingTest.java
@@ -0,0 +1,296 @@
+/*
+ * 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.compatibility.common.tradefed.loading;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.compatibility.common.tradefed.targetprep.ApkInstaller;
+import com.android.compatibility.common.tradefed.targetprep.PreconditionPreparer;
+import com.android.compatibility.common.tradefed.testtype.JarHostTest;
+import com.android.tradefed.build.FolderBuildInfo;
+import com.android.tradefed.config.ConfigurationDescriptor;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.ConfigurationFactory;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IDeviceConfiguration;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.invoker.shard.token.TokenProperty;
+import com.android.tradefed.targetprep.DeviceSetup;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.PythonVirtualenvPreparer;
+import com.android.tradefed.testtype.AndroidJUnitTest;
+import com.android.tradefed.testtype.GTest;
+import com.android.tradefed.testtype.HostTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.ITestFilterReceiver;
+import com.android.tradefed.testtype.suite.ITestSuite;
+import com.android.tradefed.testtype.suite.TestSuiteInfo;
+import com.android.tradefed.util.FileUtil;
+
+import com.google.common.base.Strings;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Test that configuration in *TS can load and have expected properties.
+ */
+@RunWith(JUnit4.class)
+public class CommonConfigLoadingTest {
+
+    private static final Pattern TODO_BUG_PATTERN = Pattern.compile(".*TODO\\(b/[0-9]+\\).*", Pattern.DOTALL);
+
+    /**
+     * List of the officially supported runners in CTS, they meet all the interfaces criteria as
+     * well as support sharding very well. Any new addition should go through a review.
+     */
+    private static final Set<String> SUPPORTED_SUITE_TEST_TYPE = new HashSet<>(Arrays.asList(
+            // Suite runners
+            "com.android.compatibility.common.tradefed.testtype.JarHostTest",
+            "com.android.compatibility.testtype.DalvikTest",
+            "com.android.compatibility.testtype.LibcoreTest",
+            "com.drawelements.deqp.runner.DeqpTestRunner",
+            // Tradefed runners
+            "com.android.tradefed.testtype.AndroidJUnitTest",
+            "com.android.tradefed.testtype.ArtRunTest",
+            "com.android.tradefed.testtype.HostTest",
+            "com.android.tradefed.testtype.GTest",
+            "com.android.tradefed.testtype.mobly.MoblyBinaryHostTest",
+            "com.android.tradefed.testtype.pandora.PtsBotTest",
+            // VTS specific runners
+            "com.android.tradefed.testtype.binary.KernelTargetTest",
+            "com.android.tradefed.testtype.python.PythonBinaryHostTest",
+            "com.android.tradefed.testtype.binary.ExecutableTargetTest",
+            "com.android.tradefed.testtype.binary.ExecutableHostTest",
+            "com.android.tradefed.testtype.rust.RustBinaryTest"
+    ));
+
+    /**
+     * In Most cases we impose the usage of the AndroidJUnitRunner because it supports all the
+     * features required (filtering, sharding, etc.). We do not typically expect people to need a
+     * different runner.
+     */
+    private static final Set<String> ALLOWED_INSTRUMENTATION_RUNNER_NAME = new HashSet<>();
+    static {
+        ALLOWED_INSTRUMENTATION_RUNNER_NAME.add("android.support.test.runner.AndroidJUnitRunner");
+        ALLOWED_INSTRUMENTATION_RUNNER_NAME.add("androidx.test.runner.AndroidJUnitRunner");
+    }
+    private static final Set<String> RUNNER_EXCEPTION = new HashSet<>();
+    static {
+        // Used for a bunch of system-api cts tests
+        RUNNER_EXCEPTION.add("repackaged.android.test.InstrumentationTestRunner");
+        // Used by a UiRendering scenario where an activity is persisted between tests
+        RUNNER_EXCEPTION.add("android.uirendering.cts.runner.UiRenderingRunner");
+        // Used to avoid crashing runner on -eng build due to Log.wtf() - b/216648699
+        RUNNER_EXCEPTION.add("com.android.server.uwb.CustomTestRunner");
+        RUNNER_EXCEPTION.add("com.android.server.wifi.CustomTestRunner");
+    }
+
+    /**
+     * Test that configuration shipped in Tradefed can be parsed.
+     * -> Exclude deprecated ApkInstaller.
+     * -> Check if host-side tests are non empty.
+     */
+    @Test
+    public void testConfigurationLoad() throws Exception {
+        String rootVar = String.format("%s_ROOT", getSuiteName().toUpperCase());
+        String suiteRoot = System.getProperty(rootVar);
+        if (Strings.isNullOrEmpty(suiteRoot)) {
+            fail(String.format("Should run within a suite context: %s doesn't exist", rootVar));
+        }
+        File testcases = new File(suiteRoot, String.format("/android-%s/testcases/", getSuiteName().toLowerCase()));
+        if (!testcases.exists()) {
+            fail(String.format("%s does not exist", testcases));
+            return;
+        }
+        Set<File> listConfigs = FileUtil.findFilesObject(testcases, ".*\\.config");
+        assertTrue(listConfigs.size() > 0);
+        // Create a FolderBuildInfo to similate the CompatibilityBuildProvider
+        FolderBuildInfo stubFolder = new FolderBuildInfo("-1", "-1");
+        stubFolder.setRootDir(new File(suiteRoot));
+        stubFolder.addBuildAttribute(CompatibilityBuildHelper.SUITE_NAME, getSuiteName().toUpperCase());
+        stubFolder.addBuildAttribute("ROOT_DIR", suiteRoot);
+        TestInformation stubTestInfo = TestInformation.newBuilder()
+                .setInvocationContext(new InvocationContext()).build();
+        stubTestInfo.executionFiles().put(FilesKey.TESTS_DIRECTORY, new File(suiteRoot));
+
+        // We expect to be able to load every single config in testcases/
+        for (File config : listConfigs) {
+            IConfiguration c = ConfigurationFactory.getInstance()
+                    .createConfigurationFromArgs(new String[] {config.getAbsolutePath()});
+            if (c.getDeviceConfig().size() > 2) {
+                throw new ConfigurationException(String.format("%s declares more than 2 devices.", config));
+            }
+            int deviceCount = 0;
+            for (IDeviceConfiguration dConfig : c.getDeviceConfig()) {
+                // Ensure the deprecated ApkInstaller is not used anymore.
+                for (ITargetPreparer prep : dConfig.getTargetPreparers()) {
+                    if (prep.getClass().isAssignableFrom(ApkInstaller.class)) {
+                        throw new ConfigurationException(
+                                String.format("%s: Use com.android.tradefed.targetprep.suite."
+                                        + "SuiteApkInstaller instead of com.android.compatibility."
+                                        + "common.tradefed.targetprep.ApkInstaller, options will be "
+                                        + "the same.", config));
+                    }
+                    if (prep.getClass().isAssignableFrom(PreconditionPreparer.class)) {
+                        throw new ConfigurationException(
+                                String.format(
+                                        "%s: includes a PreconditionPreparer (%s) which is not "
+                                                + "allowed in modules.",
+                                        config.getName(), prep.getClass()));
+                    }
+                    if (prep.getClass().isAssignableFrom(DeviceSetup.class)) {
+                       DeviceSetup deviceSetup = (DeviceSetup) prep;
+                       if (!deviceSetup.isForceSkipSystemProps()) {
+                           throw new ConfigurationException(
+                                   String.format("%s: %s needs to be configured with "
+                                           + "<option name=\"force-skip-system-props\" "
+                                           + "value=\"true\" /> in *TS.",
+                                                 config.getName(), prep.getClass()));
+                       }
+                    }
+                    if (prep.getClass().isAssignableFrom(PythonVirtualenvPreparer.class)) {
+                        // Ensure each modules has a tracking bug to be imported.
+                        checkPythonModules(config, deviceCount);
+                    }
+                }
+                deviceCount++;
+            }
+            // We can ensure that Host side tests are not empty.
+            for (IRemoteTest test : c.getTests()) {
+                // Check that all the tests runners are well supported.
+                if (!SUPPORTED_SUITE_TEST_TYPE.contains(test.getClass().getCanonicalName())) {
+                    throw new ConfigurationException(
+                            String.format(
+                                    "testtype %s is not officially supported by *TS. "
+                                            + "The supported ones are: %s",
+                                    test.getClass().getCanonicalName(), SUPPORTED_SUITE_TEST_TYPE));
+                }
+                if (test instanceof HostTest) {
+                    HostTest hostTest = (HostTest) test;
+                    // We inject a made up folder so that it can find the tests.
+                    hostTest.setBuild(stubFolder);
+                    hostTest.setTestInformation(stubTestInfo);
+                    int testCount = hostTest.countTestCases();
+                    if (testCount == 0) {
+                        throw new ConfigurationException(
+                                String.format("%s: %s reports 0 test cases.",
+                                        config.getName(), test));
+                    }
+                }
+                if (test instanceof GTest) {
+                    if (((GTest) test).isRebootBeforeTestEnabled()) {
+                        throw new ConfigurationException(String.format(
+                                "%s: instead of reboot-before-test use a RebootTargetPreparer "
+                                + "which is more optimized during sharding.", config.getName()));
+                    }
+                }
+                // Tests are expected to implement that interface.
+                if (!(test instanceof ITestFilterReceiver)) {
+                    throw new IllegalArgumentException(String.format(
+                            "Test in module %s must implement ITestFilterReceiver.",
+                            config.getName()));
+                }
+                // Ensure that the device runner is the AJUR one if explicitly specified.
+                if (test instanceof AndroidJUnitTest) {
+                    AndroidJUnitTest instru = (AndroidJUnitTest) test;
+                    if (instru.getRunnerName() != null &&
+                            !ALLOWED_INSTRUMENTATION_RUNNER_NAME.contains(instru.getRunnerName())) {
+                        // Some runner are exempt
+                        if (!RUNNER_EXCEPTION.contains(instru.getRunnerName())) {
+                            throw new ConfigurationException(
+                                    String.format("%s: uses '%s' instead of on of '%s' that are "
+                                            + "expected", config.getName(), instru.getRunnerName(),
+                                            ALLOWED_INSTRUMENTATION_RUNNER_NAME));
+                        }
+                    }
+                }
+            }
+
+            ConfigurationDescriptor cd = c.getConfigurationDescription();
+            Assert.assertNotNull(config + ": configuration descriptor is null", cd);
+
+            // Check that specified tokens are expected
+            checkTokens(config.getName(), cd.getMetaData(ITestSuite.TOKEN_KEY));
+
+            // Check not-shardable: JarHostTest cannot create empty shards so it should never need
+            // to be not-shardable.
+            if (cd.isNotShardable()) {
+                for (IRemoteTest test : c.getTests()) {
+                    if (test.getClass().isAssignableFrom(JarHostTest.class)) {
+                        throw new ConfigurationException(
+                                String.format("config: %s. JarHostTest does not need the "
+                                    + "not-shardable option.", config.getName()));
+                    }
+                }
+            }
+            // Ensure options have been set
+            c.validateOptions();
+        }
+    }
+
+    /** Test that all tokens can be resolved. */
+    private void checkTokens(String configName, List<String> tokens) throws ConfigurationException {
+        if (tokens == null) {
+            return;
+        }
+        for (String token : tokens) {
+            try {
+                TokenProperty.valueOf(token.toUpperCase());
+            } catch (IllegalArgumentException e) {
+                throw new ConfigurationException(
+                        String.format(
+                                "Config: %s includes an unknown token '%s'.", configName, token));
+            }
+        }
+    }
+
+    /**
+     * For each usage of python virtualenv preparer, make sure we have tracking bugs to import as
+     * source the python libs.
+     */
+    private void checkPythonModules(File config, int deviceCount)
+            throws IOException, ConfigurationException {
+        if (deviceCount != 0) {
+            throw new ConfigurationException(
+                    String.format("%s: PythonVirtualenvPreparer should only be declared for "
+                            + "the first <device> tag in the config", config.getName()));
+        }
+        if (!TODO_BUG_PATTERN.matcher(FileUtil.readStringFromFile(config)).matches()) {
+            throw new ConfigurationException(
+                    String.format("%s: Contains some virtualenv python lib usage but no "
+                            + "tracking bug to import them as source.", config.getName()));
+        }
+    }
+
+    private String getSuiteName() {
+        return TestSuiteInfo.getInstance().getName();
+    }
+}
diff --git a/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/loading/CtsConfigLoadingTest.java b/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/loading/CtsConfigLoadingTest.java
new file mode 100644
index 0000000..40f2fad2
--- /dev/null
+++ b/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/loading/CtsConfigLoadingTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.tradefed.loading;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.FolderBuildInfo;
+import com.android.tradefed.config.ConfigurationDescriptor;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.ConfigurationFactory;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.testtype.suite.ITestSuite;
+import com.android.tradefed.testtype.suite.TestSuiteInfo;
+import com.android.tradefed.testtype.suite.params.ModuleParameters;
+import com.android.tradefed.util.FileUtil;
+
+import com.google.common.base.Strings;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Test CTS specific config requirements.
+ */
+@RunWith(JUnit4.class)
+public class CtsConfigLoadingTest {
+
+    private static final String METADATA_COMPONENT = "component";
+    private static final Set<String> KNOWN_COMPONENTS =
+            new HashSet<>(
+                    Arrays.asList(
+                            // modifications to the list below must be reviewed
+                            "abuse",
+                            "art",
+                            "auth",
+                            "auto",
+                            "autofill",
+                            "backup",
+                            "bionic",
+                            "bluetooth",
+                            "camera",
+                            "contentcapture",
+                            "deviceinfo",
+                            "deqp",
+                            "devtools",
+                            "framework",
+                            "graphics",
+                            "hdmi",
+                            "inputmethod",
+                            "libcore",
+                            "libnativehelper",
+                            "location",
+                            "media",
+                            "metrics",
+                            "misc",
+                            "mocking",
+                            "networking",
+                            "neuralnetworks",
+                            "print",
+                            "renderscript",
+                            "security",
+                            "statsd",
+                            "systems",
+                            "sysui",
+                            "telecom",
+                            "tv",
+                            "uitoolkit",
+                            "uwb",
+                            "vr",
+                            "webview",
+                            "wifi"));
+    private static final Set<String> KNOWN_MISC_MODULES =
+            new HashSet<>(
+                    Arrays.asList(
+                            // Modifications to the list below must be approved by someone in
+                            // test/suite_harness/OWNERS.
+                            "CtsSliceTestCases.config",
+                            "CtsSampleDeviceTestCases.config",
+                            "CtsUsbTests.config",
+                            "CtsGpuToolsHostTestCases.config",
+                            "CtsEdiHostTestCases.config",
+                            "CtsClassLoaderFactoryPathClassLoaderTestCases.config",
+                            "CtsSampleHostTestCases.config",
+                            "CtsHardwareTestCases.config",
+                            "CtsMonkeyTestCases.config",
+                            "CtsAndroidAppTestCases.config",
+                            "CtsClassLoaderFactoryInMemoryDexClassLoaderTestCases.config",
+                            "CtsAppComponentFactoryTestCases.config",
+                            "CtsSeccompHostTestCases.config"));
+
+
+    /**
+     * Families of module parameterization that MUST be specified explicitly in the module
+     * AndroidTest.xml.
+     */
+    private static final Set<String> MANDATORY_PARAMETERS_FAMILY = new HashSet<>();
+
+    static {
+        MANDATORY_PARAMETERS_FAMILY.add(ModuleParameters.INSTANT_APP_FAMILY);
+        MANDATORY_PARAMETERS_FAMILY.add(ModuleParameters.MULTI_ABI_FAMILY);
+        MANDATORY_PARAMETERS_FAMILY.add(ModuleParameters.SECONDARY_USER_FAMILY);
+    }
+
+    /**
+     * AllowList to start enforcing metadata on modules. No additional entry will be allowed! This
+     * is meant to burn down the remaining modules definition.
+     */
+    private static final Set<String> ALLOWLIST_MODULE_PARAMETERS = new HashSet<>();
+
+    static {
+    }
+
+    /**
+     * Test that configuration shipped in Tradefed can be parsed.
+     * -> Exclude deprecated ApkInstaller.
+     * -> Check if host-side tests are non empty.
+     */
+    @Test
+    public void testConfigurationLoad() throws Exception {
+        String rootVar = String.format("%s_ROOT", getSuiteName().toUpperCase());
+        String suiteRoot = System.getProperty(rootVar);
+        if (Strings.isNullOrEmpty(suiteRoot)) {
+            fail(String.format("Should run within a suite context: %s doesn't exist", rootVar));
+        }
+        File testcases = new File(suiteRoot, String.format("/android-%s/testcases/", getSuiteName().toLowerCase()));
+        if (!testcases.exists()) {
+            fail(String.format("%s does not exists", testcases));
+            return;
+        }
+        Set<File> listConfigs = FileUtil.findFilesObject(testcases, ".*\\.config");
+        assertTrue(listConfigs.size() > 0);
+        // Create a FolderBuildInfo to similate the CompatibilityBuildProvider
+        FolderBuildInfo stubFolder = new FolderBuildInfo("-1", "-1");
+        stubFolder.setRootDir(new File(suiteRoot));
+        stubFolder.addBuildAttribute(CompatibilityBuildHelper.SUITE_NAME, getSuiteName().toUpperCase());
+        stubFolder.addBuildAttribute("ROOT_DIR", suiteRoot);
+        TestInformation stubTestInfo = TestInformation.newBuilder()
+                .setInvocationContext(new InvocationContext()).build();
+        stubTestInfo.executionFiles().put(FilesKey.TESTS_DIRECTORY, new File(suiteRoot));
+
+        List<String> missingMandatoryParameters = new ArrayList<>();
+        // We expect to be able to load every single config in testcases/
+        for (File config : listConfigs) {
+            IConfiguration c = ConfigurationFactory.getInstance()
+                    .createConfigurationFromArgs(new String[] {config.getAbsolutePath()});
+
+            ConfigurationDescriptor cd = c.getConfigurationDescription();
+            Assert.assertNotNull(config + ": configuration descriptor is null", cd);
+            List<String> component = cd.getMetaData(METADATA_COMPONENT);
+            Assert.assertNotNull(String.format("Missing module metadata field \"component\", "
+                    + "please add the following line to your AndroidTest.xml:\n"
+                    + "<option name=\"config-descriptor:metadata\" key=\"component\" "
+                    + "value=\"...\" />\nwhere \"value\" must be one of: %s\n"
+                    + "config: %s", KNOWN_COMPONENTS, config),
+                    component);
+            Assert.assertEquals(String.format("Module config contains more than one \"component\" "
+                    + "metadata field: %s\nconfig: %s", component, config),
+                    1, component.size());
+            String cmp = component.get(0);
+            Assert.assertTrue(String.format("Module config contains unknown \"component\" metadata "
+                    + "field \"%s\", supported ones are: %s\nconfig: %s",
+                    cmp, KNOWN_COMPONENTS, config), KNOWN_COMPONENTS.contains(cmp));
+
+            if ("misc".equals(cmp)) {
+                String configFileName = config.getName();
+                Assert.assertTrue(
+                        String.format(
+                                "Adding new module %s to \"misc\" component is restricted, "
+                                        + "please pick a component that your module fits in",
+                                configFileName),
+                        KNOWN_MISC_MODULES.contains(configFileName));
+            }
+
+            // Check that specified parameters are expected
+            boolean res =
+                    checkModuleParameters(
+                            config.getName(), cd.getMetaData(ITestSuite.PARAMETER_KEY));
+            if (!res) {
+                missingMandatoryParameters.add(config.getName());
+            }
+
+            String suiteName = getSuiteName().toLowerCase();
+            // Ensure each CTS module is tagged with <option name="test-suite-tag" value="cts" />
+            Assert.assertTrue(String.format(
+                    "Module config %s does not contains "
+                    + "'<option name=\"test-suite-tag\" value=\"%s\" />'", config.getName(), suiteName),
+                    cd.getSuiteTags().contains(suiteName));
+
+            // Ensure options have been set
+            c.validateOptions();
+        }
+
+        // Exempt the allow list
+        missingMandatoryParameters.removeAll(ALLOWLIST_MODULE_PARAMETERS);
+        // Ensure the mandatory fields are filled
+        if (!missingMandatoryParameters.isEmpty()) {
+            String msg =
+                    String.format(
+                            "The following %s modules are missing some of the mandatory "
+                                    + "parameters [instant_app, not_instant_app, "
+                                    + "multi_abi, not_multi_abi, "
+                                    + "secondary_user, not_secondary_user]: '%s'",
+                            missingMandatoryParameters.size(), missingMandatoryParameters);
+            throw new ConfigurationException(msg);
+        }
+    }
+
+    /** Test that all parameter metadata can be resolved. */
+    private boolean checkModuleParameters(String configName, List<String> parameters)
+            throws ConfigurationException {
+        if (parameters == null) {
+            return false;
+        }
+        Map<String, Boolean> families = createFamilyCheckMap();
+        for (String param : parameters) {
+            try {
+                ModuleParameters p = ModuleParameters.valueOf(param.toUpperCase());
+                if (families.containsKey(p.getFamily())) {
+                    families.put(p.getFamily(), true);
+                }
+            } catch (IllegalArgumentException e) {
+                throw new ConfigurationException(
+                        String.format("Config: %s includes an unknown parameter '%s'.",
+                                configName, param));
+            }
+        }
+        if (families.containsValue(false)) {
+            return false;
+        }
+        return true;
+    }
+
+    private Map<String, Boolean> createFamilyCheckMap() {
+        Map<String, Boolean> families = new HashMap<>();
+        for (String family : MANDATORY_PARAMETERS_FAMILY) {
+            families.put(family, false);
+        }
+        return families;
+    }
+
+    private String getSuiteName() {
+        return TestSuiteInfo.getInstance().getName();
+    }
+}
diff --git a/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/presubmit/ApkPackageNameCheck.java b/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/presubmit/ApkPackageNameCheck.java
index 1ee5503..9db955c 100644
--- a/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/presubmit/ApkPackageNameCheck.java
+++ b/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/presubmit/ApkPackageNameCheck.java
@@ -114,8 +114,10 @@
                     } else if (prep instanceof PushFilePreparer) {
                         for (File f : ((PushFilePreparer) prep).getPushSpecs(null).values()) {
                             String path = f.getPath();
-                            File toBePushed = FileUtil.findFile(config.getParentFile(), path);
-                            if (toBePushed == null) {
+                            // Use findFiles to also match top-level dir, which is a valid push spec
+                            Set<String> toBePushed = FileUtil.findFiles(config.getParentFile(),
+                                                                        path);
+                            if (toBePushed.isEmpty()) {
                                 // TODO: Enforce should abort on failure is True in CTS
                                 if (((PushFilePreparer) prep).shouldAbortOnFailure()) {
                                     errors.add(
diff --git a/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/presubmit/ValidateTestsAbi.java b/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/presubmit/ValidateTestsAbi.java
index 94a65bf..306737f 100644
--- a/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/presubmit/ValidateTestsAbi.java
+++ b/tools/cts-tradefed/tests/src/com/android/compatibility/common/tradefed/presubmit/ValidateTestsAbi.java
@@ -105,6 +105,7 @@
         BINARY_EXCEPTIONS.add("img2simg");
         BINARY_EXCEPTIONS.add("lpmake");
         BINARY_EXCEPTIONS.add("lpunpack");
+        BINARY_EXCEPTIONS.add("mk_payload");
         BINARY_EXCEPTIONS.add("sign_virt_apex");
         BINARY_EXCEPTIONS.add("simg2img");
     }
@@ -237,7 +238,11 @@
                 }
                 try {
                     // Ignore python binaries
-                    if (FileUtil.readStringFromFile(f).startsWith("#!/usr/bin/env python")) {
+                    String content = FileUtil.readStringFromFile(f);
+                    if (content.startsWith("#!/usr/bin/env python")) {
+                        return true;
+                    }
+                    if (content.contains("mobly/__init__.py")) {
                         return true;
                     }
                 } catch (IOException e) {
diff --git a/tools/cts-tradefed/tests/src/com/android/compatibility/tradefed/CtsUnitTests.java b/tools/cts-tradefed/tests/src/com/android/compatibility/tradefed/CtsUnitTests.java
index 821491d..3b8e591 100644
--- a/tools/cts-tradefed/tests/src/com/android/compatibility/tradefed/CtsUnitTests.java
+++ b/tools/cts-tradefed/tests/src/com/android/compatibility/tradefed/CtsUnitTests.java
@@ -15,8 +15,9 @@
  */
 package com.android.compatibility.tradefed;
 
+import com.android.compatibility.common.tradefed.loading.CommonConfigLoadingTest;
+import com.android.compatibility.common.tradefed.loading.CtsConfigLoadingTest;
 import com.android.compatibility.common.tradefed.presubmit.ApkPackageNameCheck;
-import com.android.compatibility.common.tradefed.presubmit.CtsConfigLoadingTest;
 import com.android.compatibility.common.tradefed.presubmit.PresubmitSetupValidation;
 import com.android.compatibility.common.tradefed.presubmit.ValidateTestsAbi;
 
@@ -34,9 +35,13 @@
     // base
     CtsTradefedTest.class,
 
+    // loading test
+    CommonConfigLoadingTest.class,
+    CtsConfigLoadingTest.class,
+
     // presubmit
     ApkPackageNameCheck.class,
-    CtsConfigLoadingTest.class,
+
     PresubmitSetupValidation.class,
     ValidateTestsAbi.class,
 })